//! The representation of a `#[doc(cfg(...))]` attribute. // FIXME: Once the portability lint RFC is implemented (see tracking issue #41619), // switch to use those structures instead. use std::fmt::{self, Write}; use std::mem; use std::ops; use rustc_ast::{LitKind, MetaItem, MetaItemKind, NestedMetaItem}; use rustc_data_structures::fx::FxHashSet; use rustc_feature::Features; use rustc_session::parse::ParseSess; use rustc_span::symbol::{sym, Symbol}; use rustc_span::Span; use crate::html::escape::Escape; #[cfg(test)] mod tests; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) enum Cfg { /// Accepts all configurations. True, /// Denies all configurations. False, /// A generic configuration option, e.g., `test` or `target_os = "linux"`. Cfg(Symbol, Option), /// Negates a configuration requirement, i.e., `not(x)`. Not(Box), /// Union of a list of configuration requirements, i.e., `any(...)`. Any(Vec), /// Intersection of a list of configuration requirements, i.e., `all(...)`. All(Vec), } #[derive(PartialEq, Debug)] pub(crate) struct InvalidCfgError { pub(crate) msg: &'static str, pub(crate) span: Span, } impl Cfg { /// Parses a `NestedMetaItem` into a `Cfg`. fn parse_nested( nested_cfg: &NestedMetaItem, exclude: &FxHashSet, ) -> Result, InvalidCfgError> { match nested_cfg { NestedMetaItem::MetaItem(ref cfg) => Cfg::parse_without(cfg, exclude), NestedMetaItem::Lit(ref lit) => { Err(InvalidCfgError { msg: "unexpected literal", span: lit.span }) } } } pub(crate) fn parse_without( cfg: &MetaItem, exclude: &FxHashSet, ) -> Result, InvalidCfgError> { let name = match cfg.ident() { Some(ident) => ident.name, None => { return Err(InvalidCfgError { msg: "expected a single identifier", span: cfg.span, }); } }; match cfg.kind { MetaItemKind::Word => { let cfg = Cfg::Cfg(name, None); if exclude.contains(&cfg) { Ok(None) } else { Ok(Some(cfg)) } } MetaItemKind::NameValue(ref lit) => match lit.kind { LitKind::Str(value, _) => { let cfg = Cfg::Cfg(name, Some(value)); if exclude.contains(&cfg) { Ok(None) } else { Ok(Some(cfg)) } } _ => Err(InvalidCfgError { // FIXME: if the main #[cfg] syntax decided to support non-string literals, // this should be changed as well. msg: "value of cfg option should be a string literal", span: lit.span, }), }, MetaItemKind::List(ref items) => { let orig_len = items.len(); let sub_cfgs = items.iter().filter_map(|i| Cfg::parse_nested(i, exclude).transpose()); let ret = match name { sym::all => sub_cfgs.fold(Ok(Cfg::True), |x, y| Ok(x? & y?)), sym::any => sub_cfgs.fold(Ok(Cfg::False), |x, y| Ok(x? | y?)), sym::not => { if orig_len == 1 { let mut sub_cfgs = sub_cfgs.collect::>(); if sub_cfgs.len() == 1 { Ok(!sub_cfgs.pop().unwrap()?) } else { return Ok(None); } } else { Err(InvalidCfgError { msg: "expected 1 cfg-pattern", span: cfg.span }) } } _ => Err(InvalidCfgError { msg: "invalid predicate", span: cfg.span }), }; match ret { Ok(c) => Ok(Some(c)), Err(e) => Err(e), } } } } /// Parses a `MetaItem` into a `Cfg`. /// /// The `MetaItem` should be the content of the `#[cfg(...)]`, e.g., `unix` or /// `target_os = "redox"`. /// /// If the content is not properly formatted, it will return an error indicating what and where /// the error is. pub(crate) fn parse(cfg: &MetaItem) -> Result { Self::parse_without(cfg, &FxHashSet::default()).map(|ret| ret.unwrap()) } /// Checks whether the given configuration can be matched in the current session. /// /// Equivalent to `attr::cfg_matches`. // FIXME: Actually make use of `features`. pub(crate) fn matches(&self, parse_sess: &ParseSess, features: Option<&Features>) -> bool { match *self { Cfg::False => false, Cfg::True => true, Cfg::Not(ref child) => !child.matches(parse_sess, features), Cfg::All(ref sub_cfgs) => { sub_cfgs.iter().all(|sub_cfg| sub_cfg.matches(parse_sess, features)) } Cfg::Any(ref sub_cfgs) => { sub_cfgs.iter().any(|sub_cfg| sub_cfg.matches(parse_sess, features)) } Cfg::Cfg(name, value) => parse_sess.config.contains(&(name, value)), } } /// Whether the configuration consists of just `Cfg` or `Not`. fn is_simple(&self) -> bool { match *self { Cfg::False | Cfg::True | Cfg::Cfg(..) | Cfg::Not(..) => true, Cfg::All(..) | Cfg::Any(..) => false, } } /// Whether the configuration consists of just `Cfg`, `Not` or `All`. fn is_all(&self) -> bool { match *self { Cfg::False | Cfg::True | Cfg::Cfg(..) | Cfg::Not(..) | Cfg::All(..) => true, Cfg::Any(..) => false, } } /// Renders the configuration for human display, as a short HTML description. pub(crate) fn render_short_html(&self) -> String { let mut msg = Display(self, Format::ShortHtml).to_string(); if self.should_capitalize_first_letter() && let Some(i) = msg.find(|c: char| c.is_ascii_alphanumeric()) { msg[i..i + 1].make_ascii_uppercase(); } msg } /// Renders the configuration for long display, as a long HTML description. pub(crate) fn render_long_html(&self) -> String { let on = if self.should_use_with_in_description() { "with" } else { "on" }; let mut msg = format!("Available {on} {}", Display(self, Format::LongHtml)); if self.should_append_only_to_description() { msg.push_str(" only"); } msg.push('.'); msg } /// Renders the configuration for long display, as a long plain text description. pub(crate) fn render_long_plain(&self) -> String { let on = if self.should_use_with_in_description() { "with" } else { "on" }; let mut msg = format!("Available {on} {}", Display(self, Format::LongPlain)); if self.should_append_only_to_description() { msg.push_str(" only"); } msg } fn should_capitalize_first_letter(&self) -> bool { match *self { Cfg::False | Cfg::True | Cfg::Not(..) => true, Cfg::Any(ref sub_cfgs) | Cfg::All(ref sub_cfgs) => { sub_cfgs.first().map(Cfg::should_capitalize_first_letter).unwrap_or(false) } Cfg::Cfg(name, _) => name == sym::debug_assertions || name == sym::target_endian, } } fn should_append_only_to_description(&self) -> bool { match *self { Cfg::False | Cfg::True => false, Cfg::Any(..) | Cfg::All(..) | Cfg::Cfg(..) => true, Cfg::Not(box Cfg::Cfg(..)) => true, Cfg::Not(..) => false, } } fn should_use_with_in_description(&self) -> bool { matches!(self, Cfg::Cfg(sym::target_feature, _)) } /// Attempt to simplify this cfg by assuming that `assume` is already known to be true, will /// return `None` if simplification managed to completely eliminate any requirements from this /// `Cfg`. /// /// See `tests::test_simplify_with` for examples. pub(crate) fn simplify_with(&self, assume: &Cfg) -> Option { if self == assume { return None; } if let Cfg::All(a) = self { let mut sub_cfgs: Vec = if let Cfg::All(b) = assume { a.iter().filter(|a| !b.contains(a)).cloned().collect() } else { a.iter().filter(|&a| a != assume).cloned().collect() }; let len = sub_cfgs.len(); return match len { 0 => None, 1 => sub_cfgs.pop(), _ => Some(Cfg::All(sub_cfgs)), }; } else if let Cfg::All(b) = assume { if b.contains(self) { return None; } } Some(self.clone()) } } impl ops::Not for Cfg { type Output = Cfg; fn not(self) -> Cfg { match self { Cfg::False => Cfg::True, Cfg::True => Cfg::False, Cfg::Not(cfg) => *cfg, s => Cfg::Not(Box::new(s)), } } } impl ops::BitAndAssign for Cfg { fn bitand_assign(&mut self, other: Cfg) { match (self, other) { (&mut Cfg::False, _) | (_, Cfg::True) => {} (s, Cfg::False) => *s = Cfg::False, (s @ &mut Cfg::True, b) => *s = b, (&mut Cfg::All(ref mut a), Cfg::All(ref mut b)) => { for c in b.drain(..) { if !a.contains(&c) { a.push(c); } } } (&mut Cfg::All(ref mut a), ref mut b) => { if !a.contains(b) { a.push(mem::replace(b, Cfg::True)); } } (s, Cfg::All(mut a)) => { let b = mem::replace(s, Cfg::True); if !a.contains(&b) { a.push(b); } *s = Cfg::All(a); } (s, b) => { if *s != b { let a = mem::replace(s, Cfg::True); *s = Cfg::All(vec![a, b]); } } } } } impl ops::BitAnd for Cfg { type Output = Cfg; fn bitand(mut self, other: Cfg) -> Cfg { self &= other; self } } impl ops::BitOrAssign for Cfg { fn bitor_assign(&mut self, other: Cfg) { match (self, other) { (Cfg::True, _) | (_, Cfg::False) | (_, Cfg::True) => {} (s @ &mut Cfg::False, b) => *s = b, (&mut Cfg::Any(ref mut a), Cfg::Any(ref mut b)) => { for c in b.drain(..) { if !a.contains(&c) { a.push(c); } } } (&mut Cfg::Any(ref mut a), ref mut b) => { if !a.contains(b) { a.push(mem::replace(b, Cfg::True)); } } (s, Cfg::Any(mut a)) => { let b = mem::replace(s, Cfg::True); if !a.contains(&b) { a.push(b); } *s = Cfg::Any(a); } (s, b) => { if *s != b { let a = mem::replace(s, Cfg::True); *s = Cfg::Any(vec![a, b]); } } } } } impl ops::BitOr for Cfg { type Output = Cfg; fn bitor(mut self, other: Cfg) -> Cfg { self |= other; self } } #[derive(Clone, Copy)] enum Format { LongHtml, LongPlain, ShortHtml, } impl Format { fn is_long(self) -> bool { match self { Format::LongHtml | Format::LongPlain => true, Format::ShortHtml => false, } } fn is_html(self) -> bool { match self { Format::LongHtml | Format::ShortHtml => true, Format::LongPlain => false, } } } /// Pretty-print wrapper for a `Cfg`. Also indicates what form of rendering should be used. struct Display<'a>(&'a Cfg, Format); fn write_with_opt_paren( fmt: &mut fmt::Formatter<'_>, has_paren: bool, obj: T, ) -> fmt::Result { if has_paren { fmt.write_char('(')?; } obj.fmt(fmt)?; if has_paren { fmt.write_char(')')?; } Ok(()) } impl<'a> fmt::Display for Display<'a> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match *self.0 { Cfg::Not(ref child) => match **child { Cfg::Any(ref sub_cfgs) => { let separator = if sub_cfgs.iter().all(Cfg::is_simple) { " nor " } else { ", nor " }; for (i, sub_cfg) in sub_cfgs.iter().enumerate() { fmt.write_str(if i == 0 { "neither " } else { separator })?; write_with_opt_paren(fmt, !sub_cfg.is_all(), Display(sub_cfg, self.1))?; } Ok(()) } ref simple @ Cfg::Cfg(..) => write!(fmt, "non-{}", Display(simple, self.1)), ref c => write!(fmt, "not ({})", Display(c, self.1)), }, Cfg::Any(ref sub_cfgs) => { let separator = if sub_cfgs.iter().all(Cfg::is_simple) { " or " } else { ", or " }; let short_longhand = self.1.is_long() && { let all_crate_features = sub_cfgs .iter() .all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::feature, Some(_)))); let all_target_features = sub_cfgs .iter() .all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::target_feature, Some(_)))); if all_crate_features { fmt.write_str("crate features ")?; true } else if all_target_features { fmt.write_str("target features ")?; true } else { false } }; for (i, sub_cfg) in sub_cfgs.iter().enumerate() { if i != 0 { fmt.write_str(separator)?; } if let (true, Cfg::Cfg(_, Some(feat))) = (short_longhand, sub_cfg) { if self.1.is_html() { write!(fmt, "{feat}")?; } else { write!(fmt, "`{feat}`")?; } } else { write_with_opt_paren(fmt, !sub_cfg.is_all(), Display(sub_cfg, self.1))?; } } Ok(()) } Cfg::All(ref sub_cfgs) => { let short_longhand = self.1.is_long() && { let all_crate_features = sub_cfgs .iter() .all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::feature, Some(_)))); let all_target_features = sub_cfgs .iter() .all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::target_feature, Some(_)))); if all_crate_features { fmt.write_str("crate features ")?; true } else if all_target_features { fmt.write_str("target features ")?; true } else { false } }; for (i, sub_cfg) in sub_cfgs.iter().enumerate() { if i != 0 { fmt.write_str(" and ")?; } if let (true, Cfg::Cfg(_, Some(feat))) = (short_longhand, sub_cfg) { if self.1.is_html() { write!(fmt, "{feat}")?; } else { write!(fmt, "`{feat}`")?; } } else { write_with_opt_paren(fmt, !sub_cfg.is_simple(), Display(sub_cfg, self.1))?; } } Ok(()) } Cfg::True => fmt.write_str("everywhere"), Cfg::False => fmt.write_str("nowhere"), Cfg::Cfg(name, value) => { let human_readable = match (name, value) { (sym::unix, None) => "Unix", (sym::windows, None) => "Windows", (sym::debug_assertions, None) => "debug-assertions enabled", (sym::target_os, Some(os)) => match os.as_str() { "android" => "Android", "dragonfly" => "DragonFly BSD", "emscripten" => "Emscripten", "freebsd" => "FreeBSD", "fuchsia" => "Fuchsia", "haiku" => "Haiku", "hermit" => "HermitCore", "illumos" => "illumos", "ios" => "iOS", "l4re" => "L4Re", "linux" => "Linux", "macos" => "macOS", "netbsd" => "NetBSD", "openbsd" => "OpenBSD", "redox" => "Redox", "solaris" => "Solaris", "tvos" => "tvOS", "wasi" => "WASI", "watchos" => "watchOS", "windows" => "Windows", _ => "", }, (sym::target_arch, Some(arch)) => match arch.as_str() { "aarch64" => "AArch64", "arm" => "ARM", "asmjs" => "JavaScript", "loongarch64" => "LoongArch LA64", "m68k" => "M68k", "csky" => "CSKY", "mips" => "MIPS", "mips32r6" => "MIPS Release 6", "mips64" => "MIPS-64", "mips64r6" => "MIPS-64 Release 6", "msp430" => "MSP430", "powerpc" => "PowerPC", "powerpc64" => "PowerPC-64", "riscv32" => "RISC-V RV32", "riscv64" => "RISC-V RV64", "s390x" => "s390x", "sparc64" => "SPARC64", "wasm32" | "wasm64" => "WebAssembly", "x86" => "x86", "x86_64" => "x86-64", _ => "", }, (sym::target_vendor, Some(vendor)) => match vendor.as_str() { "apple" => "Apple", "pc" => "PC", "sun" => "Sun", "fortanix" => "Fortanix", _ => "", }, (sym::target_env, Some(env)) => match env.as_str() { "gnu" => "GNU", "msvc" => "MSVC", "musl" => "musl", "newlib" => "Newlib", "uclibc" => "uClibc", "sgx" => "SGX", _ => "", }, (sym::target_endian, Some(endian)) => return write!(fmt, "{endian}-endian"), (sym::target_pointer_width, Some(bits)) => return write!(fmt, "{bits}-bit"), (sym::target_feature, Some(feat)) => match self.1 { Format::LongHtml => { return write!(fmt, "target feature {feat}"); } Format::LongPlain => return write!(fmt, "target feature `{feat}`"), Format::ShortHtml => return write!(fmt, "{feat}"), }, (sym::feature, Some(feat)) => match self.1 { Format::LongHtml => { return write!(fmt, "crate feature {feat}"); } Format::LongPlain => return write!(fmt, "crate feature `{feat}`"), Format::ShortHtml => return write!(fmt, "{feat}"), }, _ => "", }; if !human_readable.is_empty() { fmt.write_str(human_readable) } else if let Some(v) = value { if self.1.is_html() { write!( fmt, r#"{}="{}""#, Escape(name.as_str()), Escape(v.as_str()) ) } else { write!(fmt, r#"`{name}="{v}"`"#) } } else if self.1.is_html() { write!(fmt, "{}", Escape(name.as_str())) } else { write!(fmt, "`{name}`") } } } } }