From ee9b61cf754f17c979cdd6722c1dd6e3b27a101b Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Sat, 14 Mar 2026 15:59:32 +0100 Subject: [PATCH 01/12] deprecate `std::char` constants and functions similar to how constants in those modules for numeric types have been deprecated --- library/core/src/char/mod.rs | 23 +++++++++++++---------- library/coretests/tests/lib.rs | 1 - 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/library/core/src/char/mod.rs b/library/core/src/char/mod.rs index 82a3f6f916be3..f7e0c533d31ad 100644 --- a/library/core/src/char/mod.rs +++ b/library/core/src/char/mod.rs @@ -93,31 +93,31 @@ const MAX_THREE_B: u32 = 0x10000; /// The highest valid code point a `char` can have, `'\u{10FFFF}'`. Use [`char::MAX`] instead. #[stable(feature = "rust1", since = "1.0.0")] +#[deprecated(since = "TBD", note = "replaced by the `MAX` associated constant on `char`")] pub const MAX: char = char::MAX; -/// The maximum number of bytes required to [encode](char::encode_utf8) a `char` to -/// UTF-8 encoding. -#[unstable(feature = "char_max_len", issue = "121714")] -pub const MAX_LEN_UTF8: usize = char::MAX_LEN_UTF8; - -/// The maximum number of two-byte units required to [encode](char::encode_utf16) a `char` -/// to UTF-16 encoding. -#[unstable(feature = "char_max_len", issue = "121714")] -pub const MAX_LEN_UTF16: usize = char::MAX_LEN_UTF16; - /// `U+FFFD REPLACEMENT CHARACTER` (�) is used in Unicode to represent a /// decoding error. Use [`char::REPLACEMENT_CHARACTER`] instead. #[stable(feature = "decode_utf16", since = "1.9.0")] +#[deprecated( + since = "TBD", + note = "replaced by the `REPLACEMENT_CHARACTER` associated constant on `char`" +)] pub const REPLACEMENT_CHARACTER: char = char::REPLACEMENT_CHARACTER; /// The version of [Unicode](https://www.unicode.org/) that the Unicode parts of /// `char` and `str` methods are based on. Use [`char::UNICODE_VERSION`] instead. #[stable(feature = "unicode_version", since = "1.45.0")] +#[deprecated( + since = "TBD", + note = "replaced by the `UNICODE_VERSION` associated constant on `char`" +)] pub const UNICODE_VERSION: (u8, u8, u8) = char::UNICODE_VERSION; /// Creates an iterator over the UTF-16 encoded code points in `iter`, returning /// unpaired surrogates as `Err`s. Use [`char::decode_utf16`] instead. #[stable(feature = "decode_utf16", since = "1.9.0")] +#[deprecated(since = "TBD", note = "replaced by the `decode_utf16` method on `char`")] #[inline] pub fn decode_utf16>(iter: I) -> DecodeUtf16 { self::decode::decode_utf16(iter) @@ -126,6 +126,7 @@ pub fn decode_utf16>(iter: I) -> DecodeUtf16 Option { @@ -136,6 +137,7 @@ pub const fn from_u32(i: u32) -> Option { /// instead. #[stable(feature = "char_from_unchecked", since = "1.5.0")] #[rustc_const_stable(feature = "const_char_from_u32_unchecked", since = "1.81.0")] +#[deprecated(since = "TBD", note = "replaced by the `from_u32_unchecked` method on `char`")] #[must_use] #[inline] pub const unsafe fn from_u32_unchecked(i: u32) -> char { @@ -146,6 +148,7 @@ pub const unsafe fn from_u32_unchecked(i: u32) -> char { /// Converts a digit in the given radix to a `char`. Use [`char::from_digit`] instead. #[stable(feature = "rust1", since = "1.0.0")] #[rustc_const_stable(feature = "const_char_convert", since = "1.67.0")] +#[deprecated(since = "TBD", note = "replaced by the `from_digit` method on `char`")] #[must_use] #[inline] pub const fn from_digit(num: u32, radix: u32) -> Option { diff --git a/library/coretests/tests/lib.rs b/library/coretests/tests/lib.rs index 72112f8b01133..8d3f5fabad657 100644 --- a/library/coretests/tests/lib.rs +++ b/library/coretests/tests/lib.rs @@ -11,7 +11,6 @@ #![feature(bstr)] #![feature(cfg_target_has_reliable_f16_f128)] #![feature(char_internals)] -#![feature(char_max_len)] #![feature(clone_to_uninit)] #![feature(const_array)] #![feature(const_bool)] From bbb9e3bb4a40f82e39a0d30b34fcaa5f8c2d1c6c Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 5 Apr 2026 19:39:33 -0700 Subject: [PATCH 02/12] libtest: use binary search for --exact test filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test array is sorted by name at compile time. When `--exact` is passed in, use binary search for O(f log n) lookups instead of an O(n) linear scan, under the assumption that f << n (which is true for the most relevant cases). This is important for Miri, where the interpreted execution makes the linear scan very expensive. I measured this against a repo with 1000 empty tests, running `cargo +stage1 miri nextest run test_00` (100 tests) under hyperfine: * Before (linear scan): 49.7s ± 0.6s * After (binary search): 41.9s ± 0.2s (-15.7%) I also tried a few other variations (particularly swapping matching tests to the front of the list + truncating the list), but the index + swap_remove approach proved to be the fastest. Questions: - [ ] To be conservative, I've assumed that test_main can potentially receive an unsorted list of tests. Is this assumption correct? --- .../rustc_builtin_macros/src/test_harness.rs | 2 + library/test/src/console.rs | 9 +- library/test/src/lib.rs | 95 +++++++++++++++---- library/test/src/tests.rs | 16 ++-- library/test/src/types.rs | 27 ++++++ 5 files changed, 121 insertions(+), 28 deletions(-) diff --git a/compiler/rustc_builtin_macros/src/test_harness.rs b/compiler/rustc_builtin_macros/src/test_harness.rs index 1bb6d8a6bfd05..8ede26cede12b 100644 --- a/compiler/rustc_builtin_macros/src/test_harness.rs +++ b/compiler/rustc_builtin_macros/src/test_harness.rs @@ -370,6 +370,8 @@ fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> Box { let ecx = &cx.ext_cx; let mut tests = cx.test_cases.clone(); + // Note that this sort is load-bearing: the libtest harness uses binary search to find tests by + // name. tests.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str())); ecx.expr_array_ref( diff --git a/library/test/src/console.rs b/library/test/src/console.rs index 13b2b3d502c81..b1c5404a7160c 100644 --- a/library/test/src/console.rs +++ b/library/test/src/console.rs @@ -16,7 +16,7 @@ use super::helpers::metrics::MetricMap; use super::options::{Options, OutputFormat}; use super::test_result::TestResult; use super::time::{TestExecTime, TestSuiteExecTime}; -use super::types::{NamePadding, TestDesc, TestDescAndFn}; +use super::types::{NamePadding, TestDesc, TestDescAndFn, TestList}; use super::{filter_tests, run_tests, term}; /// Generic wrapper over stdout. @@ -170,7 +170,7 @@ impl ConsoleTestState { } // List the tests to console, and optionally to logfile. Filters are honored. -pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec) -> io::Result<()> { +pub(crate) fn list_tests_console(opts: &TestOpts, tests: TestList) -> io::Result<()> { let output = match term::stdout() { None => OutputLocation::Raw(io::stdout().lock()), Some(t) => OutputLocation::Pretty(t), @@ -186,7 +186,7 @@ pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec) -> let mut st = ConsoleTestDiscoveryState::new(opts)?; out.write_discovery_start()?; - for test in filter_tests(opts, tests).into_iter() { + for test in filter_tests(opts, tests) { use crate::TestFn::*; let TestDescAndFn { desc, testfn } = test; @@ -307,8 +307,9 @@ pub(crate) fn get_formatter(opts: &TestOpts, max_name_len: usize) -> Box) -> io::Result { +pub fn run_tests_console(opts: &TestOpts, tests: TestList) -> io::Result { let max_name_len = tests + .tests .iter() .max_by_key(|t| len_if_padded(t)) .map(|t| t.desc.name.as_slice().len()) diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index f3dbd3d0556ab..791af5f8ee2a9 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -49,7 +49,7 @@ pub mod test { pub use crate::time::{TestExecTime, TestTimeOptions}; pub use crate::types::{ DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, - TestDescAndFn, TestId, TestName, TestType, + TestDescAndFn, TestId, TestList, TestListOrder, TestName, TestType, }; pub use crate::{assert_test_result, filter_tests, run_test, test_main, test_main_static}; } @@ -106,6 +106,16 @@ pub fn test_main_with_exit_callback( tests: Vec, options: Option, exit_callback: F, +) { + let tests = TestList::new(tests, TestListOrder::Unsorted); + test_main_inner(args, tests, options, exit_callback) +} + +fn test_main_inner( + args: &[String], + tests: TestList, + options: Option, + exit_callback: F, ) { let mut opts = match cli::parse_opts(args) { Some(Ok(o)) => o, @@ -180,7 +190,9 @@ pub fn test_main_with_exit_callback( pub fn test_main_static(tests: &[&TestDescAndFn]) { let args = env::args().collect::>(); let owned_tests: Vec<_> = tests.iter().map(make_owned_test).collect(); - test_main(&args, owned_tests, None) + // Tests are sorted by name at compile time by mk_tests_slice. + let tests = TestList::new(owned_tests, TestListOrder::Sorted); + test_main_inner(&args, tests, None, || {}) } /// A variant optimized for invocation with a static test vector. @@ -229,7 +241,9 @@ pub fn test_main_static_abort(tests: &[&TestDescAndFn]) { let args = env::args().collect::>(); let owned_tests: Vec<_> = tests.iter().map(make_owned_test).collect(); - test_main(&args, owned_tests, Some(Options::new().panic_abort(true))) + // Tests are sorted by name at compile time by mk_tests_slice. + let tests = TestList::new(owned_tests, TestListOrder::Sorted); + test_main_inner(&args, tests, Some(Options::new().panic_abort(true)), || {}) } /// Clones static values for putting into a dynamic vector, which test_main() @@ -298,7 +312,7 @@ impl FilteredTests { pub fn run_tests( opts: &TestOpts, - tests: Vec, + tests: TestList, mut notify_about_test_event: F, ) -> io::Result<()> where @@ -334,7 +348,7 @@ where timeout: Instant, } - let tests_len = tests.len(); + let tests_len = tests.tests.len(); let mut filtered = FilteredTests { tests: Vec::new(), benches: Vec::new(), next_id: 0 }; @@ -512,25 +526,48 @@ where Ok(()) } -pub fn filter_tests(opts: &TestOpts, tests: Vec) -> Vec { +pub fn filter_tests(opts: &TestOpts, tests: TestList) -> Vec { + let TestList { tests, order } = tests; let mut filtered = tests; - let matches_filter = |test: &TestDescAndFn, filter: &str| { - let test_name = test.desc.name.as_slice(); - - match opts.filter_exact { - true => test_name == filter, - false => test_name.contains(filter), - } - }; - // Remove tests that don't match the test filter + // Remove tests that don't match the test filter. if !opts.filters.is_empty() { - filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter))); + if opts.filter_exact && order == TestListOrder::Sorted { + // Let's say that `f` is the number of filters and `n` is the number + // of tests. + // + // The test array is sorted by name (guaranteed by the caller via + // TestListOrder::Sorted), so use binary search for O(f log n) + // exact-match lookups instead of an O(n) linear scan. + // + // This is important for Miri, where the interpreted execution makes + // the linear scan very expensive. + filtered = filter_exact_match(filtered, &opts.filters); + } else { + filtered.retain(|test| { + let test_name = test.desc.name.as_slice(); + opts.filters.iter().any(|filter| { + if opts.filter_exact { + test_name == filter.as_str() + } else { + test_name.contains(filter.as_str()) + } + }) + }); + } } // Skip tests that match any of the skip filters + // + // After exact positive filtering above, the filtered set is small, so a + // linear scan is acceptable even under Miri. if !opts.skip.is_empty() { - filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf))); + filtered.retain(|test| { + let name = test.desc.name.as_slice(); + !opts.skip.iter().any(|sf| { + if opts.filter_exact { name == sf.as_str() } else { name.contains(sf.as_str()) } + }) + }); } // Excludes #[should_panic] tests @@ -553,6 +590,30 @@ pub fn filter_tests(opts: &TestOpts, tests: Vec) -> Vec, filters: &[String]) -> Vec { + // Binary search for each filter in the sorted test list. + let mut indexes: Vec = filters + .iter() + .filter_map(|f| tests.binary_search_by(|t| t.desc.name.as_slice().cmp(f.as_str())).ok()) + .collect(); + indexes.sort_unstable(); + indexes.dedup(); + + // Extract matching tests. Process indexes in descending order so that + // swap_remove (which replaces the removed element with the last) does not + // invalidate indexes we haven't visited yet. + let mut result = Vec::with_capacity(indexes.len()); + for &idx in indexes.iter().rev() { + result.push(tests.swap_remove(idx)); + } + // Reverse to restore the original sorted order, since we extracted the + // matching tests in descending index order. + result.reverse(); + result +} + pub fn convert_benchmarks_to_tests(tests: Vec) -> Vec { // convert benchmarks to tests, if we're not benchmarking them tests diff --git a/library/test/src/tests.rs b/library/test/src/tests.rs index d986bd74f772b..b25462cce1f99 100644 --- a/library/test/src/tests.rs +++ b/library/test/src/tests.rs @@ -477,7 +477,7 @@ fn filter_for_ignored_option() { opts.run_tests = true; opts.run_ignored = RunIgnored::Only; - let tests = one_ignored_one_unignored_test(); + let tests = TestList::new(one_ignored_one_unignored_test(), TestListOrder::Unsorted); let filtered = filter_tests(&opts, tests); assert_eq!(filtered.len(), 1); @@ -494,7 +494,7 @@ fn run_include_ignored_option() { opts.run_tests = true; opts.run_ignored = RunIgnored::Yes; - let tests = one_ignored_one_unignored_test(); + let tests = TestList::new(one_ignored_one_unignored_test(), TestListOrder::Unsorted); let filtered = filter_tests(&opts, tests); assert_eq!(filtered.len(), 2); @@ -527,7 +527,7 @@ fn exclude_should_panic_option() { testfn: DynTestFn(Box::new(move || Ok(()))), }); - let filtered = filter_tests(&opts, tests); + let filtered = filter_tests(&opts, TestList::new(tests, TestListOrder::Unsorted)); assert_eq!(filtered.len(), 2); assert!(filtered.iter().all(|test| test.desc.should_panic == ShouldPanic::No)); @@ -535,8 +535,8 @@ fn exclude_should_panic_option() { #[test] fn exact_filter_match() { - fn tests() -> Vec { - ["base", "base::test", "base::test1", "base::test2"] + fn tests() -> TestList { + let tests = ["base", "base::test", "base::test1", "base::test2"] .into_iter() .map(|name| TestDescAndFn { desc: TestDesc { @@ -555,7 +555,8 @@ fn exact_filter_match() { }, testfn: DynTestFn(Box::new(move || Ok(()))), }) - .collect() + .collect(); + TestList::new(tests, TestListOrder::Sorted) } let substr = @@ -908,7 +909,8 @@ fn test_dyn_bench_returning_err_fails_when_run_as_test() { } Ok(()) }; - run_tests(&TestOpts { run_tests: true, ..TestOpts::new() }, vec![desc], notify).unwrap(); + let tests = TestList::new(vec![desc], TestListOrder::Unsorted); + run_tests(&TestOpts { run_tests: true, ..TestOpts::new() }, tests, notify).unwrap(); let result = rx.recv().unwrap().result; assert_eq!(result, TrFailed); } diff --git a/library/test/src/types.rs b/library/test/src/types.rs index 802cab989c6a9..14c81bc2d1cf1 100644 --- a/library/test/src/types.rs +++ b/library/test/src/types.rs @@ -284,3 +284,30 @@ impl TestDescAndFn { } } } + +/// Whether a [`TestList`]'s tests are known to be sorted by name. +/// +/// When tests are sorted, `filter_tests` can use binary search for `--exact` +/// matches instead of a linear scan. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TestListOrder { + /// Tests are sorted by name. This is guaranteed for tests generated by + /// `rustc --test` (see `mk_tests_slice` in + /// `compiler/rustc_builtin_macros/src/test_harness.rs`). + Sorted, + /// Test order is unknown; binary search must not be used. + Unsorted, +} + +/// A list of tests, tagged with whether they are sorted by name. +#[derive(Debug)] +pub struct TestList { + pub tests: Vec, + pub order: TestListOrder, +} + +impl TestList { + pub fn new(tests: Vec, order: TestListOrder) -> Self { + Self { tests, order } + } +} From 782a214fd667559b8e67fdba726927056740cb6c Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Sat, 11 Apr 2026 16:15:29 -0400 Subject: [PATCH 03/12] Implemented PermissionsExt ACP on Windows, which provides functions/utilities to observe, set, and create a Permissions struct with certain file attributes --- library/std/src/os/windows/fs.rs | 65 ++++++++++++++++++++++++++++++- library/std/src/sys/fs/windows.rs | 10 +++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index 7fd46b31f7d83..14e8466323cd6 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -4,11 +4,11 @@ #![stable(feature = "rust1", since = "1.0.0")] -use crate::fs::{self, Metadata, OpenOptions}; +use crate::fs::{self, Metadata, OpenOptions, Permissions}; use crate::io::BorrowedCursor; use crate::path::Path; use crate::sealed::Sealed; -use crate::sys::{AsInner, AsInnerMut, IntoInner}; +use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner}; use crate::time::SystemTime; use crate::{io, sys}; @@ -368,6 +368,67 @@ impl OpenOptionsExt2 for OpenOptions { } } +/// Windows-specific extensions to [`fs::Permissions`]. This extension trait +/// provides extra utilities to shows what Windows file attributes are enabled +/// in [`Permissions`] and to manually set file attributes on [`Permissions`]. +/// +/// See Microsoft's [`File Attribute Constants`] page to know what file +/// attribute metadata are defined and stored on Windows files. +/// +/// [`Permissions`]: fs::Permissions +/// [`File Attribute Constants`]: +/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants +/// +/// # Example +/// +/// ```no_run +/// use std::fs::Permissions; +/// use std::os::windows::fs::PermissionsExt; +/// +/// const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4; +/// const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20; +/// let my_file_attr = FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE; +/// let mut permissions = Permissions::from_file_attributes(my_file_attr); +/// assert_eq!(permissions.file_attributes(), my_file_attr); +/// +/// const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; +/// let new_file_attr = permissions.file_attributes() | FILE_ATTRIBUTE_HIDDEN; +/// permissions.set_file_attributes(new_file_attr); +/// assert_eq!(permissions.file_attributes(), new_file_attr); +/// ``` +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +pub trait PermissionsExt: Sealed { + /// Returns the file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn file_attributes(&self) -> u32; + + /// Sets the file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn set_file_attributes(&mut self, mask: u32); + + /// Creates a new instance from the given file attribute bits. + #[unstable(feature = "windows_permissions_ext", issue = "152956")] + fn from_file_attributes(mask: u32) -> Self; +} + +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +impl Sealed for fs::Permissions {} + +#[unstable(feature = "windows_permissions_ext", issue = "152956")] +impl PermissionsExt for fs::Permissions { + fn file_attributes(&self) -> u32 { + self.as_inner().file_attributes() + } + + fn set_file_attributes(&mut self, mask: u32) { + *self = Permissions::from_inner(FromInner::from_inner(mask)); + } + + fn from_file_attributes(mask: u32) -> Self { + Permissions::from_inner(FromInner::from_inner(mask)) + } +} + /// Windows-specific extensions to [`fs::Metadata`]. /// /// The data members that this trait exposes correspond to the members diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 74854cdeb498d..e0b02670264d9 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -1167,6 +1167,16 @@ impl FilePermissions { self.attrs &= !c::FILE_ATTRIBUTE_READONLY; } } + + pub fn file_attributes(&self) -> u32 { + self.attrs as u32 + } +} + +impl FromInner for FilePermissions { + fn from_inner(attrs: u32) -> FilePermissions { + FilePermissions { attrs } + } } impl FileTimes { From 8712a924272a4af3596f86b920b3e7fe50e2179f Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sat, 4 Apr 2026 18:35:32 +0100 Subject: [PATCH 04/12] Replace the spdx-rs dependency with a minimal in-tree SPDX tag-value parser The spdx-rs crate is no longer maintained and is behind on its own dependency updates. The only function that collect-license-metadata uses from it is `spdx_rs::parsers::spdx_from_tag_value`, which parses the output of the `reuse` tool to extract file names, licences and copyright text. Replace this with a small minimal parser that handles just the subset of the SPDX tag-value format that is needed: `Tag: Value` line pairs and multi-line `...` blocks. Coincidentally, this gets rid of the last transitive dependency on syn v1. --- Cargo.lock | 149 +++++------------- src/tools/collect-license-metadata/Cargo.toml | 1 - .../collect-license-metadata/src/main.rs | 1 + .../collect-license-metadata/src/reuse.rs | 13 +- .../collect-license-metadata/src/spdx.rs | 99 ++++++++++++ 5 files changed, 146 insertions(+), 117 deletions(-) create mode 100644 src/tools/collect-license-metadata/src/spdx.rs diff --git a/Cargo.lock b/Cargo.lock index 563d99d5475c4..916a335ca9821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,7 +199,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_derive", - "syn 2.0.110", + "syn", ] [[package]] @@ -396,7 +396,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -573,7 +573,6 @@ checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", - "serde", "windows-link 0.2.1", ] @@ -635,10 +634,10 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -768,7 +767,6 @@ dependencies = [ "serde", "serde_json", "similar", - "spdx-rs", ] [[package]] @@ -804,7 +802,7 @@ dependencies = [ "nom", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1047,7 +1045,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.110", + "syn", ] [[package]] @@ -1061,7 +1059,7 @@ dependencies = [ "indexmap", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1079,7 +1077,7 @@ dependencies = [ "indexmap", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1103,7 +1101,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn", ] [[package]] @@ -1114,7 +1112,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1146,7 +1144,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1167,7 +1165,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1177,7 +1175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.110", + "syn", ] [[package]] @@ -1189,7 +1187,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1264,7 +1262,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -1677,12 +1675,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -2083,7 +2075,7 @@ checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -2380,7 +2372,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -2525,7 +2517,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -2906,7 +2898,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -3116,7 +3108,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -3307,7 +3299,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -3400,7 +3392,7 @@ checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -4115,7 +4107,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -4261,7 +4253,7 @@ dependencies = [ "fluent-syntax", "proc-macro2", "quote", - "syn 2.0.110", + "syn", "synstructure", ] @@ -4843,7 +4835,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", "synstructure", ] @@ -4943,7 +4935,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", + "syn", ] [[package]] @@ -5060,7 +5052,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.110", + "syn", ] [[package]] @@ -5146,7 +5138,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -5157,7 +5149,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -5290,35 +5282,6 @@ dependencies = [ "color-eyre", ] -[[package]] -name = "spdx-expression" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d7ac03c67c572d85049d6db815e20a4a19b41b3d5cca732ac582342021ad77" -dependencies = [ - "nom", - "serde", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "spdx-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990870190ec8d8c64ba66e4a6746243d6e57d99353991e0e6092334833f429b1" -dependencies = [ - "chrono", - "log", - "nom", - "serde", - "spdx-expression", - "strum", - "strum_macros", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5384,36 +5347,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.110" @@ -5433,7 +5366,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -5570,7 +5503,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -5581,7 +5514,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -5823,7 +5756,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -6019,7 +5952,7 @@ checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ "proc-macro-hack", "quote", - "syn 2.0.110", + "syn", "unic-langid-impl", ] @@ -6257,7 +6190,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn", "wasm-bindgen-shared", ] @@ -6514,7 +6447,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -6525,7 +6458,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -6895,7 +6828,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", "synstructure", ] @@ -6916,7 +6849,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] @@ -6936,7 +6869,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", "synstructure", ] @@ -6971,7 +6904,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn", ] [[package]] diff --git a/src/tools/collect-license-metadata/Cargo.toml b/src/tools/collect-license-metadata/Cargo.toml index f84da24428155..ae41d2fbb009b 100644 --- a/src/tools/collect-license-metadata/Cargo.toml +++ b/src/tools/collect-license-metadata/Cargo.toml @@ -10,4 +10,3 @@ anyhow = "1.0.65" serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.85" similar = "2.7.0" -spdx-rs = "0.5.1" diff --git a/src/tools/collect-license-metadata/src/main.rs b/src/tools/collect-license-metadata/src/main.rs index 4e218ea59fda6..156871b1b3a80 100644 --- a/src/tools/collect-license-metadata/src/main.rs +++ b/src/tools/collect-license-metadata/src/main.rs @@ -1,6 +1,7 @@ mod licenses; mod path_tree; mod reuse; +mod spdx; use std::path::PathBuf; diff --git a/src/tools/collect-license-metadata/src/reuse.rs b/src/tools/collect-license-metadata/src/reuse.rs index dbe46781b7c5b..6bc41453a53ff 100644 --- a/src/tools/collect-license-metadata/src/reuse.rs +++ b/src/tools/collect-license-metadata/src/reuse.rs @@ -15,18 +15,15 @@ pub(crate) fn collect( let raw = &obtain_spdx_document(reuse_exe)?; println!("finished gathering the license information from REUSE in {:.2?}", start.elapsed()); - let document = spdx_rs::parsers::spdx_from_tag_value(&raw)?; + let files = crate::spdx::parse_tag_value(raw)?; let mut result = Vec::new(); - for file in document.file_information { - let concluded_license = file.concluded_license.expect("File should have licence info"); - let copyright_text = file.copyright_text.expect("File should have copyright text"); + for file in files { let license = interner.intern(License { - spdx: concluded_license.to_string(), - copyright: copyright_text.split('\n').map(|s| s.into()).collect(), + spdx: file.concluded_license, + copyright: file.copyright_text.split('\n').map(|s| s.into()).collect(), }); - - result.push((file.file_name.into(), license)); + result.push((file.name.into(), license)); } Ok(result) diff --git a/src/tools/collect-license-metadata/src/spdx.rs b/src/tools/collect-license-metadata/src/spdx.rs new file mode 100644 index 0000000000000..11dd72ee2c38c --- /dev/null +++ b/src/tools/collect-license-metadata/src/spdx.rs @@ -0,0 +1,99 @@ +use anyhow::Error; + +/// A single file entry extracted from an SPDX tag-value document. +pub(crate) struct SpdxFileEntry { + pub(crate) name: String, + pub(crate) concluded_license: String, + pub(crate) copyright_text: String, +} + +/// Parses an SPDX tag-value document and extracts file information. +/// +/// This is a minimal parser that only extracts the fields we need +/// (FileName, LicenseConcluded, FileCopyrightText) rather than the full model. +/// The format is specified by the SPDX specification: +/// each line is a `Tag: Value` pair, +/// and multi-line values are wrapped in ``. +pub(crate) fn parse_tag_value(input: &str) -> Result, Error> { + let mut files = Vec::new(); + let mut current_name: Option = None; + let mut current_license: Option = None; + let mut current_copyright: Option = None; + + let mut lines = input.lines(); + while let Some(line) = lines.next() { + let Some((tag, value)) = line.split_once(": ") else { + continue; + }; + + let value = resolve_multiline_value(value, &mut lines)?; + + match tag { + "FileName" => { + // A new file section begins. Flush the previous one if present. + if let Some(name) = current_name.take() { + files.push(build_file_entry( + name, + current_license.take(), + current_copyright.take(), + )?); + } + current_name = Some(value); + current_license = None; + current_copyright = None; + } + "LicenseConcluded" => current_license = Some(value), + "FileCopyrightText" => current_copyright = Some(value), + _ => {} + } + } + + // Flush the last file section. + if let Some(name) = current_name { + files.push(build_file_entry(name, current_license, current_copyright)?); + } + + Ok(files) +} + +/// Resolves a tag value that might span multiple lines using ``. +fn resolve_multiline_value<'a>( + value: &str, + further_lines: &mut impl Iterator, +) -> Result { + let Some(start) = value.strip_prefix("") else { + return Ok(value.to_string()); + }; + + // The closing tag might be on the same line. + if let Some(content) = start.strip_suffix("") { + return Ok(content.to_string()); + } + + let mut text = start.to_string(); + for line in further_lines.by_ref() { + if let Some(rest) = line.strip_suffix("") { + text.push('\n'); + text.push_str(rest); + return Ok(text); + } + text.push('\n'); + text.push_str(line); + } + + anyhow::bail!("unexpected end of input inside block") +} + +fn build_file_entry( + name: String, + concluded_license: Option, + copyright_text: Option, +) -> Result { + Ok(SpdxFileEntry { + name, + concluded_license: concluded_license + .ok_or_else(|| anyhow::anyhow!("file missing LicenseConcluded"))?, + copyright_text: copyright_text + .ok_or_else(|| anyhow::anyhow!("file missing FileCopyrightText"))?, + }) +} From 57de72c2e2240ff59e7c49aeb4e2017e65261919 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 12 Apr 2026 21:42:17 +0100 Subject: [PATCH 05/12] Add a bunch of unit tests for the in-house parser --- .../collect-license-metadata/src/spdx.rs | 3 + .../src/spdx/tests.rs | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/tools/collect-license-metadata/src/spdx/tests.rs diff --git a/src/tools/collect-license-metadata/src/spdx.rs b/src/tools/collect-license-metadata/src/spdx.rs index 11dd72ee2c38c..a94f2bcf51eca 100644 --- a/src/tools/collect-license-metadata/src/spdx.rs +++ b/src/tools/collect-license-metadata/src/spdx.rs @@ -97,3 +97,6 @@ fn build_file_entry( .ok_or_else(|| anyhow::anyhow!("file missing FileCopyrightText"))?, }) } + +#[cfg(test)] +mod tests; diff --git a/src/tools/collect-license-metadata/src/spdx/tests.rs b/src/tools/collect-license-metadata/src/spdx/tests.rs new file mode 100644 index 0000000000000..5b7cb411931dc --- /dev/null +++ b/src/tools/collect-license-metadata/src/spdx/tests.rs @@ -0,0 +1,134 @@ +use super::*; + +// Clause 8.1 ("File name field") specifies that each file section begins with +// a `FileName` tag whose value is a relative path prefixed with "./". +// Clause 8.5 ("Concluded license") and 8.8 ("Copyright text") give the +// corresponding per-file fields. +// +#[test] +fn single_file_entry() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: LGPL-2.0-only +FileCopyrightText: Copyright 2008-2010 John Smith"; + + let files = parse_tag_value(input).unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].name, "./package/foo.c"); + assert_eq!(files[0].concluded_license, "LGPL-2.0-only"); + assert_eq!(files[0].copyright_text, "Copyright 2008-2010 John Smith"); +} + +// Clause 8.5 shows compound SPDX licence expressions as valid values for +// `LicenseConcluded`, e.g. "(LGPL-2.0-only OR LicenseRef-2)". +// +#[test] +fn compound_license_expression() { + let input = "\ +FileName: ./src/lib.rs +LicenseConcluded: (LGPL-2.0-only OR LicenseRef-2) +FileCopyrightText: Copyright Example Company"; + + let files = parse_tag_value(input).unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].concluded_license, "(LGPL-2.0-only OR LicenseRef-2)"); +} + +// Clause 8.8 shows the copyright text wrapped in a single-line +// ... block: e.g. +// `FileCopyrightText: Copyright 2008-2010 John Smith` +// +#[test] +fn single_line_text_block() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: LGPL-2.0-only +FileCopyrightText: Copyright 2008-2010 John Smith"; + + let files = parse_tag_value(input).unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].copyright_text, "Copyright 2008-2010 John Smith"); +} + +// Clause 6.10 ("Creator comment") demonstrates a multi-line ... block. +// +#[test] +fn multi_line_text_block() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: MIT +FileCopyrightText: Copyright 2008-2010 John Smith +Copyright 2019 Jane Doe"; + + let files = parse_tag_value(input).unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].copyright_text, "Copyright 2008-2010 John Smith\nCopyright 2019 Jane Doe"); +} + +// Clause 5 ("Composition of an SPDX document") states that a document may +// contain zero or many File Information sections. Each `FileName` tag starts +// a new section, so consecutive file blocks must be parsed independently. +// +#[test] +fn multiple_file_entries() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: LGPL-2.0-only +FileCopyrightText: Copyright 2008-2010 John Smith +FileName: ./package/bar.c +LicenseConcluded: MIT +FileCopyrightText: Copyright Example Company"; + + let files = parse_tag_value(input).unwrap(); + assert_eq!(files.len(), 2); + + assert_eq!(files[0].name, "./package/foo.c"); + assert_eq!(files[0].concluded_license, "LGPL-2.0-only"); + assert_eq!(files[0].copyright_text, "Copyright 2008-2010 John Smith"); + + assert_eq!(files[1].name, "./package/bar.c"); + assert_eq!(files[1].concluded_license, "MIT"); + assert_eq!(files[1].copyright_text, "Copyright Example Company"); +} + +// A file section without a `LicenseConcluded` tag is malformed. +#[test] +fn missing_license_is_an_error() { + let input = "\ +FileName: ./package/foo.c +FileCopyrightText: Copyright 2008-2010 John Smith"; + + assert!(parse_tag_value(input).is_err()); +} + +// A file section without a `FileCopyrightText` tag is malformed. +#[test] +fn missing_copyright_is_an_error() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: MIT"; + + assert!(parse_tag_value(input).is_err()); +} + +// A section with an unterminated block (no closing ) is malformed. +#[test] +fn unterminated_text_block_is_an_error() { + let input = "\ +FileName: ./package/foo.c +LicenseConcluded: MIT +FileCopyrightText: Copyright 2008-2010 John Smith"; + + assert!(parse_tag_value(input).is_err()); +} + +// A document with no `FileName` tags at all should produce an empty result. +#[test] +fn empty_document_returns_no_entries() { + let input = "\ +SPDXVersion: SPDX-2.3 +DataLicense: CC0-1.0"; + + let files = parse_tag_value(input).unwrap(); + assert!(files.is_empty()); +} From 1bdbde51489be49a87f99c4609cf8455ac5e6141 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 12 Apr 2026 21:58:05 +0100 Subject: [PATCH 06/12] Move spdx.rs to spdx/mod.rs --- src/tools/collect-license-metadata/src/{spdx.rs => spdx/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/tools/collect-license-metadata/src/{spdx.rs => spdx/mod.rs} (100%) diff --git a/src/tools/collect-license-metadata/src/spdx.rs b/src/tools/collect-license-metadata/src/spdx/mod.rs similarity index 100% rename from src/tools/collect-license-metadata/src/spdx.rs rename to src/tools/collect-license-metadata/src/spdx/mod.rs From 1d6177020c7439bfa666c12a83c5f29e3eb08374 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:49:55 +0200 Subject: [PATCH 07/12] Add test for coalescing of diagnostic attribute duplicates --- .../duplicate_coalescing.both.stderr | 25 +++++++++++++++++++ .../duplicate_coalescing.label.stderr | 24 ++++++++++++++++++ .../duplicate_coalescing.note.stderr | 24 ++++++++++++++++++ .../duplicate_coalescing.rs | 22 ++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 tests/ui/diagnostic_namespace/duplicate_coalescing.both.stderr create mode 100644 tests/ui/diagnostic_namespace/duplicate_coalescing.label.stderr create mode 100644 tests/ui/diagnostic_namespace/duplicate_coalescing.note.stderr create mode 100644 tests/ui/diagnostic_namespace/duplicate_coalescing.rs diff --git a/tests/ui/diagnostic_namespace/duplicate_coalescing.both.stderr b/tests/ui/diagnostic_namespace/duplicate_coalescing.both.stderr new file mode 100644 index 0000000000000..e1ccd6f078ef9 --- /dev/null +++ b/tests/ui/diagnostic_namespace/duplicate_coalescing.both.stderr @@ -0,0 +1,25 @@ +error[E0277]: this message + --> $DIR/duplicate_coalescing.rs:15:17 + | +LL | takes_trait(()); + | ----------- ^^ this label + | | + | required by a bound introduced by this call + | + = help: the trait `Trait` is not implemented for `()` + = note: a note + = note: another note +help: this trait has no implementations, consider adding one + --> $DIR/duplicate_coalescing.rs:10:1 + | +LL | trait Trait {} + | ^^^^^^^^^^^ +note: required by a bound in `takes_trait` + --> $DIR/duplicate_coalescing.rs:12:24 + | +LL | fn takes_trait(_: impl Trait) {} + | ^^^^^ required by this bound in `takes_trait` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/diagnostic_namespace/duplicate_coalescing.label.stderr b/tests/ui/diagnostic_namespace/duplicate_coalescing.label.stderr new file mode 100644 index 0000000000000..85c8e0a6984f1 --- /dev/null +++ b/tests/ui/diagnostic_namespace/duplicate_coalescing.label.stderr @@ -0,0 +1,24 @@ +error[E0277]: this message + --> $DIR/duplicate_coalescing.rs:15:17 + | +LL | takes_trait(()); + | ----------- ^^ this label + | | + | required by a bound introduced by this call + | + = help: the trait `Trait` is not implemented for `()` + = note: a note +help: this trait has no implementations, consider adding one + --> $DIR/duplicate_coalescing.rs:10:1 + | +LL | trait Trait {} + | ^^^^^^^^^^^ +note: required by a bound in `takes_trait` + --> $DIR/duplicate_coalescing.rs:12:24 + | +LL | fn takes_trait(_: impl Trait) {} + | ^^^^^ required by this bound in `takes_trait` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/diagnostic_namespace/duplicate_coalescing.note.stderr b/tests/ui/diagnostic_namespace/duplicate_coalescing.note.stderr new file mode 100644 index 0000000000000..69701d43f2499 --- /dev/null +++ b/tests/ui/diagnostic_namespace/duplicate_coalescing.note.stderr @@ -0,0 +1,24 @@ +error[E0277]: this message + --> $DIR/duplicate_coalescing.rs:15:17 + | +LL | takes_trait(()); + | ----------- ^^ the trait `Trait` is not implemented for `()` + | | + | required by a bound introduced by this call + | + = note: a note + = note: another note +help: this trait has no implementations, consider adding one + --> $DIR/duplicate_coalescing.rs:10:1 + | +LL | trait Trait {} + | ^^^^^^^^^^^ +note: required by a bound in `takes_trait` + --> $DIR/duplicate_coalescing.rs:12:24 + | +LL | fn takes_trait(_: impl Trait) {} + | ^^^^^ required by this bound in `takes_trait` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/diagnostic_namespace/duplicate_coalescing.rs b/tests/ui/diagnostic_namespace/duplicate_coalescing.rs new file mode 100644 index 0000000000000..cf610841dc8e1 --- /dev/null +++ b/tests/ui/diagnostic_namespace/duplicate_coalescing.rs @@ -0,0 +1,22 @@ +//@ revisions: label note both +//@[both] compile-flags: --cfg label --cfg note +//@ dont-require-annotations: NOTE + +//! Example and test of multiple diagnostic attributes coalescing together. + +#[diagnostic::on_unimplemented(message = "this message", note = "a note")] +#[cfg_attr(label, diagnostic::on_unimplemented(label = "this label"))] +#[cfg_attr(note, diagnostic::on_unimplemented(note = "another note"))] +trait Trait {} + +fn takes_trait(_: impl Trait) {} + +fn main() { + takes_trait(()); + //~^ERROR this message + //[label]~|NOTE this label + //[both]~|NOTE this label + //~|NOTE a note + //[note]~|NOTE another note + //[both]~|NOTE another note +} From f684713c4374f3bb53563fb7a63129e59531f98f Mon Sep 17 00:00:00 2001 From: Martin Nordholts Date: Wed, 15 Apr 2026 20:05:41 +0200 Subject: [PATCH 08/12] triagebot.toml: Sync `assign.owners` with `autolabel."T-compiler"` In `autolabel."T-compiler"`, several `./tests/*` dirs are listed. But many of them are missing from `assign.owners`. Add them all to `assign.owners` so reviewers are picked from the compiler group, and not from the small `assign.adhoc_groups.fallback` group. --- triagebot.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/triagebot.toml b/triagebot.toml index b0bf55a9248f4..5f320160320b5 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -1627,6 +1627,18 @@ dep-bumps = [ "/src/llvm-project" = ["@cuviper"] "/src/rustdoc-json-types" = ["rustdoc"] "/src/stage0" = ["bootstrap"] +"/tests/assembly-llvm" = ["compiler"] +"/tests/auxiliary" = ["compiler"] +"/tests/codegen-llvm" = ["compiler"] +"/tests/codegen-units" = ["compiler"] +"/tests/COMPILER_TESTS.md" = ["compiler"] +"/tests/coverage-run-rustdoc" = ["compiler"] +"/tests/coverage" = ["compiler"] +"/tests/crashes" = ["compiler"] +"/tests/debuginfo" = ["compiler"] +"/tests/incremental" = ["compiler"] +"/tests/mir-opt" = ["compiler"] +"/tests/pretty" = ["compiler"] "/tests/run-make" = ["compiler"] "/tests/run-make-cargo" = ["compiler"] "/tests/rustdoc-html" = ["rustdoc"] @@ -1636,6 +1648,7 @@ dep-bumps = [ "/tests/rustdoc-json" = ["@aDotInTheVoid"] "/tests/rustdoc-ui" = ["rustdoc"] "/tests/ui" = ["compiler"] +"/tests/ui-fulldeps" = ["compiler"] "/src/tools/build-manifest" = ["bootstrap"] "/src/tools/cargo" = ["@ehuss"] "/src/tools/compiletest" = ["bootstrap", "@wesleywiser", "@oli-obk", "@jieyouxu"] From 46f360abbd3c432add025cd2b15b81f0c786400c Mon Sep 17 00:00:00 2001 From: Naveen Iyer Date: Thu, 16 Apr 2026 00:56:47 +0000 Subject: [PATCH 09/12] Add regression test for dead code elimination with drop + panic Dead code elimination used to fail when a Drop impl contained a panic and a potentially-panicking external function was called after the value was created. This was fixed since 1.82 but no regression test was added. The test verifies that foo() compiles to just a call to unknown() and ret void, with no panic or panicking call in the function body. Signed-off-by: Naveen R. Iyer --- tests/codegen-llvm/issues/issue-114532.rs | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/codegen-llvm/issues/issue-114532.rs diff --git a/tests/codegen-llvm/issues/issue-114532.rs b/tests/codegen-llvm/issues/issue-114532.rs new file mode 100644 index 0000000000000..41d9effbe4c94 --- /dev/null +++ b/tests/codegen-llvm/issues/issue-114532.rs @@ -0,0 +1,34 @@ +//@ compile-flags: -Copt-level=3 +#![crate_type = "lib"] + +// Regression test for #114532. +// Dead code elimination used to fail when a Drop impl contained a panic +// and a potentially-panicking function was called after the value was created. + +struct Foo(bool); + +impl Drop for Foo { + fn drop(&mut self) { + if self.0 { + return; + } + panic!("dead"); + } +} + +// CHECK-LABEL: @foo( +// CHECK-NOT: panic +// CHECK-NOT: call void @{{.*}}panicking +// CHECK: call {{.*}} @unknown( +// CHECK-NEXT: ret void +#[no_mangle] +pub fn foo() { + let _a = Foo(true); + unsafe { + unknown(9); + } +} + +extern "Rust" { + fn unknown(x: i32) -> bool; +} From e0ef87f8808a094339605fc5e859ac5baa810c0b Mon Sep 17 00:00:00 2001 From: Lukas Bergdoll Date: Fri, 17 Apr 2026 13:02:23 +0200 Subject: [PATCH 10/12] Add temporary scope to assert_matches Addresses https://github.com/rust-lang/rust/issues/154406 in part. assert_eq will be done in a separate PR. --- library/core/src/macros/mod.rs | 8 ++++---- library/coretests/tests/macros.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/library/core/src/macros/mod.rs b/library/core/src/macros/mod.rs index 33397e56b86c5..08a12b6447e61 100644 --- a/library/core/src/macros/mod.rs +++ b/library/core/src/macros/mod.rs @@ -168,7 +168,7 @@ macro_rules! assert_ne { #[allow_internal_unstable(panic_internals)] #[rustc_macro_transparency = "semiopaque"] pub macro assert_matches { - ($left:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => { + ($left:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {{ match $left { $( $pattern )|+ $( if $guard )? => {} ref left_val => { @@ -179,8 +179,8 @@ pub macro assert_matches { ); } } - }, - ($left:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $($arg:tt)+) => { + }}, + ($left:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $($arg:tt)+) => {{ match $left { $( $pattern )|+ $( if $guard )? => {} ref left_val => { @@ -191,7 +191,7 @@ pub macro assert_matches { ); } } - }, + }}, } /// Selects code at compile-time based on `cfg` predicates. diff --git a/library/coretests/tests/macros.rs b/library/coretests/tests/macros.rs index 50b5eb63e43a7..52435b226e654 100644 --- a/library/coretests/tests/macros.rs +++ b/library/coretests/tests/macros.rs @@ -1,5 +1,7 @@ #![allow(unused_must_use)] +use std::{assert_matches, debug_assert_matches}; + #[allow(dead_code)] trait Trait { fn blah(&self); @@ -219,3 +221,24 @@ fn _matches_does_not_trigger_non_exhaustive_omitted_patterns_lint(o: core::sync: // Ordering is a #[non_exhaustive] enum from a separate crate let _m = matches!(o, core::sync::atomic::Ordering::Relaxed); } + +struct MutRefWithDrop<'a>(&'a mut u32); + +// MutRefWithDrop needs to have a non-trivial drop to encounter potential lifetime issues if the +// macros don't introduce a temporary scope. +impl Drop for MutRefWithDrop<'_> { + fn drop(&mut self) { + *self.0 = u32::MAX; + } +} + +#[test] +fn temporary_scope_introduction() { + // Fails to compile if the macros don't introduce a temporary scope, since `&mut val` would + // create a second mutable borrow while `MutRefWithDrop` still holds a unique ref. + let mut val = 0; + + (assert_matches!(*MutRefWithDrop(&mut val).0, 0), std::mem::take(&mut val)); + + (debug_assert_matches!(*MutRefWithDrop(&mut val).0, 0), std::mem::take(&mut val)); +} From 3a0c0e9cb7b3797dd8f669a317d703fe74dfbfe7 Mon Sep 17 00:00:00 2001 From: Lukas Bergdoll Date: Sat, 18 Apr 2026 15:12:33 +0200 Subject: [PATCH 11/12] Apply review feedback --- library/coretests/tests/macros.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/coretests/tests/macros.rs b/library/coretests/tests/macros.rs index 52435b226e654..9f73ebd253c3b 100644 --- a/library/coretests/tests/macros.rs +++ b/library/coretests/tests/macros.rs @@ -236,9 +236,12 @@ impl Drop for MutRefWithDrop<'_> { fn temporary_scope_introduction() { // Fails to compile if the macros don't introduce a temporary scope, since `&mut val` would // create a second mutable borrow while `MutRefWithDrop` still holds a unique ref. + // See https://github.com/rust-lang/rust/issues/154406 for reference. let mut val = 0; (assert_matches!(*MutRefWithDrop(&mut val).0, 0), std::mem::take(&mut val)); + (assert_matches!(*MutRefWithDrop(&mut val).0, 0, "msg"), std::mem::take(&mut val)); (debug_assert_matches!(*MutRefWithDrop(&mut val).0, 0), std::mem::take(&mut val)); + (debug_assert_matches!(*MutRefWithDrop(&mut val).0, 0, "msg"), std::mem::take(&mut val)); } From d6da4ea9c67347bb828f07781d8c59f2ee03e49e Mon Sep 17 00:00:00 2001 From: Connor Horman Date: Tue, 7 Apr 2026 19:20:45 -0400 Subject: [PATCH 12/12] Implement core::arch::return_address and tests Fix typo Apply suggestions from code review Wording/docs changes. Co-authored-by: Ralf Jung Change signature according to Ralf's comment Fix call to `core::intrinsics::return_address()` according to the new signature Add cranelift implementation for intrinsic Change wording on `return_address!()` to be clear that returning a null pointer is best-effort. Fix formatting of doc comment Fix mistake in cranelift codegen --- .../src/intrinsics/mod.rs | 6 +++++ compiler/rustc_codegen_llvm/src/intrinsic.rs | 6 +++++ .../rustc_codegen_ssa/src/mir/intrinsic.rs | 3 ++- .../rustc_hir_analysis/src/check/intrinsic.rs | 3 +++ compiler/rustc_span/src/symbol.rs | 1 + library/core/src/arch.rs | 26 +++++++++++++++++++ library/core/src/intrinsics/mod.rs | 13 ++++++++++ .../codegen-llvm/intrinsics/return_address.rs | 10 +++++++ 8 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/codegen-llvm/intrinsics/return_address.rs diff --git a/compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs b/compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs index 780550fc4cc74..23263284d57a6 100644 --- a/compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs +++ b/compiler/rustc_codegen_cranelift/src/intrinsics/mod.rs @@ -1529,6 +1529,12 @@ fn codegen_regular_intrinsic_call<'tcx>( fx.bcx.set_cold_block(fx.bcx.current_block().unwrap()); } + sym::return_address => { + let val = fx.bcx.ins().get_return_address(fx.pointer_type); + let val = CValue::by_val(val, ret.layout()); + ret.write_cvalue(fx, val); + } + // Unimplemented intrinsics must have a fallback body. The fallback body is obtained // by converting the `InstanceKind::Intrinsic` to an `InstanceKind::Item`. _ => { diff --git a/compiler/rustc_codegen_llvm/src/intrinsic.rs b/compiler/rustc_codegen_llvm/src/intrinsic.rs index 9742f9fb3e42e..ea38fb595d4ac 100644 --- a/compiler/rustc_codegen_llvm/src/intrinsic.rs +++ b/compiler/rustc_codegen_llvm/src/intrinsic.rs @@ -789,6 +789,12 @@ impl<'ll, 'tcx> IntrinsicCallBuilderMethods<'tcx> for Builder<'_, 'll, 'tcx> { } } + sym::return_address => { + let ty = self.type_ix(32); + let val = self.const_int(ty, 0); + self.call_intrinsic("llvm.returnaddress", &[], &[val]) + } + _ => { debug!("unknown intrinsic '{}' -- falling back to default body", name); // Call the fallback body instead of generating the intrinsic code diff --git a/compiler/rustc_codegen_ssa/src/mir/intrinsic.rs b/compiler/rustc_codegen_ssa/src/mir/intrinsic.rs index fd0c7c656ac21..209116bab01ce 100644 --- a/compiler/rustc_codegen_ssa/src/mir/intrinsic.rs +++ b/compiler/rustc_codegen_ssa/src/mir/intrinsic.rs @@ -120,7 +120,8 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { | sym::contract_checks | sym::atomic_fence | sym::atomic_singlethreadfence - | sym::caller_location => {} + | sym::caller_location + | sym::return_address => {} _ => { span_bug!( span, diff --git a/compiler/rustc_hir_analysis/src/check/intrinsic.rs b/compiler/rustc_hir_analysis/src/check/intrinsic.rs index 58454cfc489c6..efcd8b354bd36 100644 --- a/compiler/rustc_hir_analysis/src/check/intrinsic.rs +++ b/compiler/rustc_hir_analysis/src/check/intrinsic.rs @@ -180,6 +180,7 @@ fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -> hi | sym::ptr_guaranteed_cmp | sym::ptr_mask | sym::ptr_metadata + | sym::return_address | sym::rotate_left | sym::rotate_right | sym::round_ties_even_f16 @@ -812,6 +813,8 @@ pub(crate) fn check_intrinsic_type( | sym::atomic_xor => (2, 1, vec![Ty::new_mut_ptr(tcx, param(0)), param(1)], param(0)), sym::atomic_fence | sym::atomic_singlethreadfence => (0, 1, Vec::new(), tcx.types.unit), + sym::return_address => (0, 0, vec![], Ty::new_imm_ptr(tcx, tcx.types.unit)), + other => { tcx.dcx().emit_err(UnrecognizedIntrinsicFunction { span, name: other }); return; diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 981bfed363dcc..e47ec956e8de7 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1657,6 +1657,7 @@ symbols! { residual, result, result_ffi_guarantees, + return_address, return_position_impl_trait_in_trait, return_type_notation, riscv32, diff --git a/library/core/src/arch.rs b/library/core/src/arch.rs index e5078a45c6d9c..248e6fcf2da75 100644 --- a/library/core/src/arch.rs +++ b/library/core/src/arch.rs @@ -76,3 +76,29 @@ pub macro global_asm("assembly template", $(operands,)* $(options($(option),*))? pub fn breakpoint() { core::intrinsics::breakpoint(); } + +/// The `core::arch::return_address!()` macro returns a pointer with an address that corresponds to the caller of the function that invoked the `return_address!()` macro. +/// The pointer has no provenance, as if created by `core::ptr::without_provenance`. It cannot be used to read memory (other than ZSTs). +/// +/// The value returned by the macro depends highly on the architecture and compiler (including any options set). +/// In particular, it is allowed to be wrong (particularly if inlining is involved), or even contain a nonsense value. +/// The result of this macro must not be relied upon for soundness or correctness, only for debugging purposes. +/// +/// As a best effort, if a useful value cannot be determined (for example, due to limitations on the current codegen), +/// this macro tries to return a null pointer instead of nonsense (this cannot be relied upon for correctness, however). +/// +/// Formally, this function returns a pointer with a non-deterministic address and no provenance. +/// +/// This is equivalent to the gcc `__builtin_return_address(0)` intrinsic (other forms of the intrinsic are not supported). +/// Because the operation can be always performed by the compiler without crashing or causing undefined behaviour, invoking the macro is a safe operation. +/// +/// ## Example +/// ``` +/// #![feature(return_address)] +/// +/// let addr = core::arch::return_address!(); +/// println!("Caller is {addr:p}"); +/// ``` +#[unstable(feature = "return_address", issue = "154966")] +#[allow_internal_unstable(core_intrinsics)] +pub macro return_address() {{ core::intrinsics::return_address() }} diff --git a/library/core/src/intrinsics/mod.rs b/library/core/src/intrinsics/mod.rs index 94d0c7eab9227..69fdb93dcf93f 100644 --- a/library/core/src/intrinsics/mod.rs +++ b/library/core/src/intrinsics/mod.rs @@ -3589,3 +3589,16 @@ pub const fn va_copy<'f>(src: &VaList<'f>) -> VaList<'f> { pub const unsafe fn va_end(ap: &mut VaList<'_>) { /* deliberately does nothing */ } + +/// Returns the return address of the caller function (after inlining) in a best-effort manner or a null pointer if it is not supported on the current backend. +/// Returning an accurate value is a quality-of-implementation concern, but no hard guarantees are +/// made about the return value: formally, the intrinsic non-deterministically returns +/// an arbitrary pointer without provenance. +/// +/// Note that unlike most intrinsics, this is safe to call. This is because it only finds the return address of the immediate caller, which is guaranteed to be possible. +/// Other forms of the corresponding gcc or llvm intrinsic (which can have wildly unpredictable results or even crash at runtime) are not exposed. +#[rustc_intrinsic] +#[rustc_nounwind] +pub fn return_address() -> *const () { + core::ptr::null() +} diff --git a/tests/codegen-llvm/intrinsics/return_address.rs b/tests/codegen-llvm/intrinsics/return_address.rs new file mode 100644 index 0000000000000..0c049bbb4c701 --- /dev/null +++ b/tests/codegen-llvm/intrinsics/return_address.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![feature(core_intrinsics, return_address)] + +// CHECK-LABEL: @call_return_address_intrinsic +#[no_mangle] +#[inline(never)] +pub fn call_return_address_intrinsic() -> *const () { + // CHECK: call ptr @llvm.returnaddress(i32 0) + core::intrinsics::return_address() +}