From f35c85f72fc1249f0553d97482ca29fe3fa0a165 Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Tue, 8 Apr 2025 16:11:28 -0700 Subject: [PATCH] Add unstable foo::bar extern command line arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also refactors some of the crate name parsing code and adds unit tests Issue #122349 Co-authored-by: León Orell Valerian Liehr --- compiler/rustc_session/src/config.rs | 39 +------- compiler/rustc_session/src/config/externs.rs | 79 ++++++++++++++++ .../rustc_session/src/config/externs/tests.rs | 92 +++++++++++++++++++ compiler/rustc_session/src/options.rs | 2 + 4 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 compiler/rustc_session/src/config/externs.rs create mode 100644 compiler/rustc_session/src/config/externs/tests.rs diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index fc05470d941c4..202378560eed7 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -14,6 +14,7 @@ use std::str::{self, FromStr}; use std::sync::LazyLock; use std::{cmp, fmt, fs, iter}; +use externs::{ExternOpt, split_extern_opt}; use rustc_data_structures::fx::{FxHashSet, FxIndexMap}; use rustc_data_structures::stable_hasher::{StableOrd, ToStableHashKey}; use rustc_errors::emitter::HumanReadableErrorType; @@ -39,6 +40,7 @@ use crate::utils::CanonicalizedPath; use crate::{EarlyDiagCtxt, HashStableContext, Session, filesearch, lint}; mod cfg; +mod externs; mod native_libs; pub mod sigpipe; @@ -2205,44 +2207,13 @@ pub fn parse_externs( matches: &getopts::Matches, unstable_opts: &UnstableOptions, ) -> Externs { - fn is_ascii_ident(string: &str) -> bool { - let mut chars = string.chars(); - if let Some(start) = chars.next() - && (start.is_ascii_alphabetic() || start == '_') - { - chars.all(|char| char.is_ascii_alphanumeric() || char == '_') - } else { - false - } - } - let is_unstable_enabled = unstable_opts.unstable_options; let mut externs: BTreeMap = BTreeMap::new(); for arg in matches.opt_strs("extern") { - let (name, path) = match arg.split_once('=') { - None => (arg, None), - Some((name, path)) => (name.to_string(), Some(Path::new(path))), - }; - let (options, name) = match name.split_once(':') { - None => (None, name), - Some((opts, name)) => (Some(opts), name.to_string()), - }; - - if !is_ascii_ident(&name) { - let mut error = early_dcx.early_struct_fatal(format!( - "crate name `{name}` passed to `--extern` is not a valid ASCII identifier" - )); - let adjusted_name = name.replace('-', "_"); - if is_ascii_ident(&adjusted_name) { - #[allow(rustc::diagnostic_outside_of_impl)] // FIXME - error.help(format!( - "consider replacing the dashes with underscores: `{adjusted_name}`" - )); - } - error.emit(); - } + let ExternOpt { crate_name: name, path, options } = + split_extern_opt(early_dcx, unstable_opts, &arg).unwrap_or_else(|e| e.emit()); - let path = path.map(|p| CanonicalizedPath::new(p)); + let path = path.map(|p| CanonicalizedPath::new(p.as_path())); let entry = externs.entry(name.to_owned()); diff --git a/compiler/rustc_session/src/config/externs.rs b/compiler/rustc_session/src/config/externs.rs new file mode 100644 index 0000000000000..1420ee38bf214 --- /dev/null +++ b/compiler/rustc_session/src/config/externs.rs @@ -0,0 +1,79 @@ +//! This module contains code to help parse and manipulate `--extern` arguments. + +use std::path::PathBuf; + +use rustc_errors::{Diag, FatalAbort}; + +use super::UnstableOptions; +use crate::EarlyDiagCtxt; + +#[cfg(test)] +mod tests; + +/// Represents the pieces of an `--extern` argument. +pub(crate) struct ExternOpt { + pub(crate) crate_name: String, + pub(crate) path: Option, + pub(crate) options: Option, +} + +/// Breaks out the major components of an `--extern` argument. +/// +/// The options field will be a string containing comma-separated options that will need further +/// parsing and processing. +pub(crate) fn split_extern_opt<'a>( + early_dcx: &'a EarlyDiagCtxt, + unstable_opts: &UnstableOptions, + extern_opt: &str, +) -> Result> { + let (name, path) = match extern_opt.split_once('=') { + None => (extern_opt.to_string(), None), + Some((name, path)) => (name.to_string(), Some(PathBuf::from(path))), + }; + let (options, crate_name) = match name.split_once(':') { + None => (None, name), + Some((opts, crate_name)) => { + if unstable_opts.namespaced_crates && crate_name.starts_with(':') { + // If the name starts with `:`, we know this was actually something like `foo::bar` and + // not a set of options. We can just use the original name as the crate name. + (None, name) + } else { + (Some(opts.to_string()), crate_name.to_string()) + } + } + }; + + if !valid_crate_name(&crate_name, unstable_opts) { + let mut error = early_dcx.early_struct_fatal(format!( + "crate name `{crate_name}` passed to `--extern` is not a valid ASCII identifier" + )); + let adjusted_name = crate_name.replace('-', "_"); + if is_ascii_ident(&adjusted_name) { + #[allow(rustc::diagnostic_outside_of_impl)] // FIXME + error + .help(format!("consider replacing the dashes with underscores: `{adjusted_name}`")); + } + return Err(error); + } + + Ok(ExternOpt { crate_name, path, options }) +} + +fn valid_crate_name(name: &str, unstable_opts: &UnstableOptions) -> bool { + match name.split_once("::") { + Some((a, b)) if unstable_opts.namespaced_crates => is_ascii_ident(a) && is_ascii_ident(b), + Some(_) => false, + None => is_ascii_ident(name), + } +} + +fn is_ascii_ident(string: &str) -> bool { + let mut chars = string.chars(); + if let Some(start) = chars.next() + && (start.is_ascii_alphabetic() || start == '_') + { + chars.all(|char| char.is_ascii_alphanumeric() || char == '_') + } else { + false + } +} diff --git a/compiler/rustc_session/src/config/externs/tests.rs b/compiler/rustc_session/src/config/externs/tests.rs new file mode 100644 index 0000000000000..6544886951572 --- /dev/null +++ b/compiler/rustc_session/src/config/externs/tests.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; + +use super::split_extern_opt; +use crate::EarlyDiagCtxt; +use crate::config::UnstableOptions; + +/// Verifies split_extern_opt handles the supported cases. +#[test] +fn test_split_extern_opt() { + let early_dcx = EarlyDiagCtxt::new(<_>::default()); + let unstable_opts = &UnstableOptions::default(); + + let extern_opt = + split_extern_opt(&early_dcx, unstable_opts, "priv,noprelude:foo=libbar.rlib").unwrap(); + assert_eq!(extern_opt.crate_name, "foo"); + assert_eq!(extern_opt.path, Some(PathBuf::from("libbar.rlib"))); + assert_eq!(extern_opt.options, Some("priv,noprelude".to_string())); + + let extern_opt = split_extern_opt(&early_dcx, unstable_opts, "priv,noprelude:foo").unwrap(); + assert_eq!(extern_opt.crate_name, "foo"); + assert_eq!(extern_opt.path, None); + assert_eq!(extern_opt.options, Some("priv,noprelude".to_string())); + + let extern_opt = split_extern_opt(&early_dcx, unstable_opts, "foo=libbar.rlib").unwrap(); + assert_eq!(extern_opt.crate_name, "foo"); + assert_eq!(extern_opt.path, Some(PathBuf::from("libbar.rlib"))); + assert_eq!(extern_opt.options, None); + + let extern_opt = split_extern_opt(&early_dcx, unstable_opts, "foo").unwrap(); + assert_eq!(extern_opt.crate_name, "foo"); + assert_eq!(extern_opt.path, None); + assert_eq!(extern_opt.options, None); +} + +/// Tests some invalid cases for split_extern_opt. +#[test] +fn test_split_extern_opt_invalid() { + let early_dcx = EarlyDiagCtxt::new(<_>::default()); + let unstable_opts = &UnstableOptions::default(); + + // too many `:`s + let result = split_extern_opt(&early_dcx, unstable_opts, "priv:noprelude:foo=libbar.rlib"); + assert!(result.is_err()); + let _ = result.map_err(|e| e.cancel()); + + // can't nest externs without the unstable flag + let result = split_extern_opt(&early_dcx, unstable_opts, "noprelude:foo::bar=libbar.rlib"); + assert!(result.is_err()); + let _ = result.map_err(|e| e.cancel()); +} + +/// Tests some cases for split_extern_opt with nested crates like `foo::bar`. +#[test] +fn test_split_extern_opt_nested() { + let early_dcx = EarlyDiagCtxt::new(<_>::default()); + let unstable_opts = &UnstableOptions { namespaced_crates: true, ..Default::default() }; + + let extern_opt = + split_extern_opt(&early_dcx, unstable_opts, "priv,noprelude:foo::bar=libbar.rlib").unwrap(); + assert_eq!(extern_opt.crate_name, "foo::bar"); + assert_eq!(extern_opt.path, Some(PathBuf::from("libbar.rlib"))); + assert_eq!(extern_opt.options, Some("priv,noprelude".to_string())); + + let extern_opt = + split_extern_opt(&early_dcx, unstable_opts, "priv,noprelude:foo::bar").unwrap(); + assert_eq!(extern_opt.crate_name, "foo::bar"); + assert_eq!(extern_opt.path, None); + assert_eq!(extern_opt.options, Some("priv,noprelude".to_string())); + + let extern_opt = split_extern_opt(&early_dcx, unstable_opts, "foo::bar=libbar.rlib").unwrap(); + assert_eq!(extern_opt.crate_name, "foo::bar"); + assert_eq!(extern_opt.path, Some(PathBuf::from("libbar.rlib"))); + assert_eq!(extern_opt.options, None); + + let extern_opt = split_extern_opt(&early_dcx, unstable_opts, "foo::bar").unwrap(); + assert_eq!(extern_opt.crate_name, "foo::bar"); + assert_eq!(extern_opt.path, None); + assert_eq!(extern_opt.options, None); +} + +/// Tests some invalid cases for split_extern_opt with nested crates like `foo::bar`. +#[test] +fn test_split_extern_opt_nested_invalid() { + let early_dcx = EarlyDiagCtxt::new(<_>::default()); + let unstable_opts = &UnstableOptions { namespaced_crates: true, ..Default::default() }; + + // crates can only be nested one deep. + let result = + split_extern_opt(&early_dcx, unstable_opts, "priv,noprelude:foo::bar::baz=libbar.rlib"); + assert!(result.is_err()); + let _ = result.map_err(|e| e.cancel()); +} diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index c70f1500d3930..bd66f835f1201 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2331,6 +2331,8 @@ options! { "the size at which the `large_assignments` lint starts to be emitted"), mutable_noalias: bool = (true, parse_bool, [TRACKED], "emit noalias metadata for mutable references (default: yes)"), + namespaced_crates: bool = (false, parse_bool, [TRACKED], + "allow crates to be namespaced by other crates (default: no)"), next_solver: NextSolverConfig = (NextSolverConfig::default(), parse_next_solver_config, [TRACKED], "enable and configure the next generation trait solver used by rustc"), nll_facts: bool = (false, parse_bool, [UNTRACKED],