diff --git a/src/librustdoc/clean/cfg.rs b/src/librustdoc/clean/cfg.rs index aa614b73dea72..881a81b22f0f7 100644 --- a/src/librustdoc/clean/cfg.rs +++ b/src/librustdoc/clean/cfg.rs @@ -3,14 +3,18 @@ // FIXME: Once the portability lint RFC is implemented (see tracking issue #41619), // switch to use those structures instead. +use std::sync::Arc; use std::{fmt, mem, ops}; use itertools::Either; use rustc_ast::{LitKind, MetaItem, MetaItemInner, MetaItemKind, MetaItemLit}; -use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_hir::attrs::AttributeKind; +use rustc_middle::ty::TyCtxt; use rustc_session::parse::ParseSess; use rustc_span::Span; use rustc_span::symbol::{Symbol, sym}; +use {rustc_ast as ast, rustc_hir as hir}; use crate::display::{Joined as _, MaybeDisplay, Wrapped}; use crate::html::escape::Escape; @@ -600,3 +604,264 @@ impl fmt::Display for Display<'_> { } } } + +/// This type keeps track of (doc) cfg information as we go down the item tree. +#[derive(Clone, Debug)] +pub(crate) struct CfgInfo { + /// List of currently active `doc(auto_cfg(hide(...)))` cfgs, minus currently active + /// `doc(auto_cfg(show(...)))` cfgs. + hidden_cfg: FxHashSet, + /// Current computed `cfg`. Each time we enter a new item, this field is updated as well while + /// taking into account the `hidden_cfg` information. + current_cfg: Cfg, + /// Whether the `doc(auto_cfg())` feature is enabled or not at this point. + auto_cfg_active: bool, + /// If the parent item used `doc(cfg(...))`, then we don't want to overwrite `current_cfg`, + /// instead we will concatenate with it. However, if it's not the case, we need to overwrite + /// `current_cfg`. + parent_is_doc_cfg: bool, +} + +impl Default for CfgInfo { + fn default() -> Self { + Self { + hidden_cfg: FxHashSet::from_iter([ + Cfg::Cfg(sym::test, None), + Cfg::Cfg(sym::doc, None), + Cfg::Cfg(sym::doctest, None), + ]), + current_cfg: Cfg::True, + auto_cfg_active: true, + parent_is_doc_cfg: false, + } + } +} + +fn show_hide_show_conflict_error( + tcx: TyCtxt<'_>, + item_span: rustc_span::Span, + previous: rustc_span::Span, +) { + let mut diag = tcx.sess.dcx().struct_span_err( + item_span, + format!( + "same `cfg` was in `auto_cfg(hide(...))` and `auto_cfg(show(...))` on the same item" + ), + ); + diag.span_note(previous, "first change was here"); + diag.emit(); +} + +/// This functions updates the `hidden_cfg` field of the provided `cfg_info` argument. +/// +/// It also checks if a same `cfg` is present in both `auto_cfg(hide(...))` and +/// `auto_cfg(show(...))` on the same item and emits an error if it's the case. +/// +/// Because we go through a list of `cfg`s, we keep track of the `cfg`s we saw in `new_show_attrs` +/// and in `new_hide_attrs` arguments. +fn handle_auto_cfg_hide_show( + tcx: TyCtxt<'_>, + cfg_info: &mut CfgInfo, + sub_attr: &MetaItemInner, + is_show: bool, + new_show_attrs: &mut FxHashMap<(Symbol, Option), rustc_span::Span>, + new_hide_attrs: &mut FxHashMap<(Symbol, Option), rustc_span::Span>, +) { + if let MetaItemInner::MetaItem(item) = sub_attr + && let MetaItemKind::List(items) = &item.kind + { + for item in items { + // FIXME: Report in case `Cfg::parse` reports an error? + if let Ok(Cfg::Cfg(key, value)) = Cfg::parse(item) { + if is_show { + if let Some(span) = new_hide_attrs.get(&(key, value)) { + show_hide_show_conflict_error(tcx, item.span(), *span); + } else { + new_show_attrs.insert((key, value), item.span()); + } + cfg_info.hidden_cfg.remove(&Cfg::Cfg(key, value)); + } else { + if let Some(span) = new_show_attrs.get(&(key, value)) { + show_hide_show_conflict_error(tcx, item.span(), *span); + } else { + new_hide_attrs.insert((key, value), item.span()); + } + cfg_info.hidden_cfg.insert(Cfg::Cfg(key, value)); + } + } + } + } +} + +pub(crate) fn extract_cfg_from_attrs<'a, I: Iterator + Clone>( + attrs: I, + tcx: TyCtxt<'_>, + cfg_info: &mut CfgInfo, +) -> Option> { + fn single(it: T) -> Option { + let mut iter = it.into_iter(); + let item = iter.next()?; + if iter.next().is_some() { + return None; + } + Some(item) + } + + fn check_changed_auto_active_status( + changed_auto_active_status: &mut Option, + attr: &ast::MetaItem, + cfg_info: &mut CfgInfo, + tcx: TyCtxt<'_>, + new_value: bool, + ) -> bool { + if let Some(first_change) = changed_auto_active_status { + if cfg_info.auto_cfg_active != new_value { + tcx.sess + .dcx() + .struct_span_err( + vec![*first_change, attr.span], + "`auto_cfg` was disabled and enabled more than once on the same item", + ) + .emit(); + return true; + } + } else { + *changed_auto_active_status = Some(attr.span); + } + cfg_info.auto_cfg_active = new_value; + false + } + + let mut new_show_attrs = FxHashMap::default(); + let mut new_hide_attrs = FxHashMap::default(); + + let mut doc_cfg = attrs + .clone() + .filter(|attr| attr.has_name(sym::doc)) + .flat_map(|attr| attr.meta_item_list().unwrap_or_default()) + .filter(|attr| attr.has_name(sym::cfg)) + .peekable(); + // If the item uses `doc(cfg(...))`, then we ignore the other `cfg(...)` attributes. + if doc_cfg.peek().is_some() { + let sess = tcx.sess; + // We overwrite existing `cfg`. + if !cfg_info.parent_is_doc_cfg { + cfg_info.current_cfg = Cfg::True; + cfg_info.parent_is_doc_cfg = true; + } + for attr in doc_cfg { + if let Some(cfg_mi) = + attr.meta_item().and_then(|attr| rustc_expand::config::parse_cfg(attr, sess)) + { + match Cfg::parse(cfg_mi) { + Ok(new_cfg) => cfg_info.current_cfg &= new_cfg, + Err(e) => { + sess.dcx().span_err(e.span, e.msg); + } + } + } + } + } else { + cfg_info.parent_is_doc_cfg = false; + } + + let mut changed_auto_active_status = None; + + // We get all `doc(auto_cfg)`, `cfg` and `target_feature` attributes. + for attr in attrs { + if let Some(ident) = attr.ident() + && ident.name == sym::doc + && let Some(attrs) = attr.meta_item_list() + { + for attr in attrs.iter().filter(|attr| attr.has_name(sym::auto_cfg)) { + let MetaItemInner::MetaItem(attr) = attr else { + continue; + }; + match &attr.kind { + MetaItemKind::Word => { + if check_changed_auto_active_status( + &mut changed_auto_active_status, + attr, + cfg_info, + tcx, + true, + ) { + return None; + } + } + MetaItemKind::NameValue(lit) => { + if let LitKind::Bool(value) = lit.kind { + if check_changed_auto_active_status( + &mut changed_auto_active_status, + attr, + cfg_info, + tcx, + value, + ) { + return None; + } + } + } + MetaItemKind::List(sub_attrs) => { + if check_changed_auto_active_status( + &mut changed_auto_active_status, + attr, + cfg_info, + tcx, + true, + ) { + return None; + } + for sub_attr in sub_attrs.iter() { + if let Some(ident) = sub_attr.ident() + && (ident.name == sym::show || ident.name == sym::hide) + { + handle_auto_cfg_hide_show( + tcx, + cfg_info, + &sub_attr, + ident.name == sym::show, + &mut new_show_attrs, + &mut new_hide_attrs, + ); + } + } + } + } + } + } else if let hir::Attribute::Parsed(AttributeKind::TargetFeature { features, .. }) = attr { + // Treat `#[target_feature(enable = "feat")]` attributes as if they were + // `#[doc(cfg(target_feature = "feat"))]` attributes as well. + for (feature, _) in features { + cfg_info.current_cfg &= Cfg::Cfg(sym::target_feature, Some(*feature)); + } + continue; + } else if !cfg_info.parent_is_doc_cfg + && let Some(ident) = attr.ident() + && matches!(ident.name, sym::cfg | sym::cfg_trace) + && let Some(attr) = single(attr.meta_item_list()?) + && let Ok(new_cfg) = Cfg::parse(&attr) + { + cfg_info.current_cfg &= new_cfg; + } + } + + // If `doc(auto_cfg)` feature is disabled and `doc(cfg())` wasn't used, there is nothing + // to be done here. + if !cfg_info.auto_cfg_active && !cfg_info.parent_is_doc_cfg { + None + } else if cfg_info.parent_is_doc_cfg { + if cfg_info.current_cfg == Cfg::True { + None + } else { + Some(Arc::new(cfg_info.current_cfg.clone())) + } + } else { + // If `doc(auto_cfg)` feature is enabled, we want to collect all `cfg` items, we remove the + // hidden ones afterward. + match cfg_info.current_cfg.strip_hidden(&cfg_info.hidden_cfg) { + None | Some(Cfg::True) => None, + Some(cfg) => Some(Arc::new(cfg)), + } + } +} diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index c6339dd475593..4fd8d245089e7 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -58,6 +58,7 @@ use tracing::{debug, instrument}; use utils::*; use {rustc_ast as ast, rustc_hir as hir}; +pub(crate) use self::cfg::{CfgInfo, extract_cfg_from_attrs}; pub(crate) use self::types::*; pub(crate) use self::utils::{krate, register_res, synthesize_auto_trait_and_blanket_impls}; use crate::core::DocContext; diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 6e3dfa0ac221c..f3662a67bbea2 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -6,8 +6,7 @@ use std::{fmt, iter}; use arrayvec::ArrayVec; use itertools::Either; use rustc_abi::{ExternAbi, VariantIdx}; -use rustc_ast::ast::{LitKind, MetaItemInner, MetaItemKind}; -use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet}; +use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; use rustc_hir::attrs::{AttributeKind, DeprecatedSince, Deprecation}; use rustc_hir::def::{CtorKind, DefKind, Res}; use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE, LocalDefId}; @@ -922,267 +921,6 @@ pub(crate) fn hir_attr_lists<'a, I: IntoIterator>( .flatten() } -/// This type keeps track of (doc) cfg information as we go down the item tree. -#[derive(Clone, Debug)] -pub(crate) struct CfgInfo { - /// List of currently active `doc(auto_cfg(hide(...)))` cfgs, minus currently active - /// `doc(auto_cfg(show(...)))` cfgs. - hidden_cfg: FxHashSet, - /// Current computed `cfg`. Each time we enter a new item, this field is updated as well while - /// taking into account the `hidden_cfg` information. - current_cfg: Cfg, - /// Whether the `doc(auto_cfg())` feature is enabled or not at this point. - auto_cfg_active: bool, - /// If the parent item used `doc(cfg(...))`, then we don't want to overwrite `current_cfg`, - /// instead we will concatenate with it. However, if it's not the case, we need to overwrite - /// `current_cfg`. - parent_is_doc_cfg: bool, -} - -impl Default for CfgInfo { - fn default() -> Self { - Self { - hidden_cfg: FxHashSet::from_iter([ - Cfg::Cfg(sym::test, None), - Cfg::Cfg(sym::doc, None), - Cfg::Cfg(sym::doctest, None), - ]), - current_cfg: Cfg::True, - auto_cfg_active: true, - parent_is_doc_cfg: false, - } - } -} - -fn show_hide_show_conflict_error( - tcx: TyCtxt<'_>, - item_span: rustc_span::Span, - previous: rustc_span::Span, -) { - let mut diag = tcx.sess.dcx().struct_span_err( - item_span, - format!( - "same `cfg` was in `auto_cfg(hide(...))` and `auto_cfg(show(...))` on the same item" - ), - ); - diag.span_note(previous, "first change was here"); - diag.emit(); -} - -/// This functions updates the `hidden_cfg` field of the provided `cfg_info` argument. -/// -/// It also checks if a same `cfg` is present in both `auto_cfg(hide(...))` and -/// `auto_cfg(show(...))` on the same item and emits an error if it's the case. -/// -/// Because we go through a list of `cfg`s, we keep track of the `cfg`s we saw in `new_show_attrs` -/// and in `new_hide_attrs` arguments. -fn handle_auto_cfg_hide_show( - tcx: TyCtxt<'_>, - cfg_info: &mut CfgInfo, - sub_attr: &MetaItemInner, - is_show: bool, - new_show_attrs: &mut FxHashMap<(Symbol, Option), rustc_span::Span>, - new_hide_attrs: &mut FxHashMap<(Symbol, Option), rustc_span::Span>, -) { - if let MetaItemInner::MetaItem(item) = sub_attr - && let MetaItemKind::List(items) = &item.kind - { - for item in items { - // FIXME: Report in case `Cfg::parse` reports an error? - if let Ok(Cfg::Cfg(key, value)) = Cfg::parse(item) { - if is_show { - if let Some(span) = new_hide_attrs.get(&(key, value)) { - show_hide_show_conflict_error(tcx, item.span(), *span); - } else { - new_show_attrs.insert((key, value), item.span()); - } - cfg_info.hidden_cfg.remove(&Cfg::Cfg(key, value)); - } else { - if let Some(span) = new_show_attrs.get(&(key, value)) { - show_hide_show_conflict_error(tcx, item.span(), *span); - } else { - new_hide_attrs.insert((key, value), item.span()); - } - cfg_info.hidden_cfg.insert(Cfg::Cfg(key, value)); - } - } - } - } -} - -pub(crate) fn extract_cfg_from_attrs<'a, I: Iterator + Clone>( - attrs: I, - tcx: TyCtxt<'_>, - cfg_info: &mut CfgInfo, -) -> Option> { - fn single(it: T) -> Option { - let mut iter = it.into_iter(); - let item = iter.next()?; - if iter.next().is_some() { - return None; - } - Some(item) - } - - fn check_changed_auto_active_status( - changed_auto_active_status: &mut Option, - attr: &ast::MetaItem, - cfg_info: &mut CfgInfo, - tcx: TyCtxt<'_>, - new_value: bool, - ) -> bool { - if let Some(first_change) = changed_auto_active_status { - if cfg_info.auto_cfg_active != new_value { - tcx.sess - .dcx() - .struct_span_err( - vec![*first_change, attr.span], - "`auto_cfg` was disabled and enabled more than once on the same item", - ) - .emit(); - return true; - } - } else { - *changed_auto_active_status = Some(attr.span); - } - cfg_info.auto_cfg_active = new_value; - false - } - - let mut new_show_attrs = FxHashMap::default(); - let mut new_hide_attrs = FxHashMap::default(); - - let mut doc_cfg = attrs - .clone() - .filter(|attr| attr.has_name(sym::doc)) - .flat_map(|attr| attr.meta_item_list().unwrap_or_default()) - .filter(|attr| attr.has_name(sym::cfg)) - .peekable(); - // If the item uses `doc(cfg(...))`, then we ignore the other `cfg(...)` attributes. - if doc_cfg.peek().is_some() { - let sess = tcx.sess; - // We overwrite existing `cfg`. - if !cfg_info.parent_is_doc_cfg { - cfg_info.current_cfg = Cfg::True; - cfg_info.parent_is_doc_cfg = true; - } - for attr in doc_cfg { - if let Some(cfg_mi) = - attr.meta_item().and_then(|attr| rustc_expand::config::parse_cfg(attr, sess)) - { - match Cfg::parse(cfg_mi) { - Ok(new_cfg) => cfg_info.current_cfg &= new_cfg, - Err(e) => { - sess.dcx().span_err(e.span, e.msg); - } - } - } - } - } else { - cfg_info.parent_is_doc_cfg = false; - } - - let mut changed_auto_active_status = None; - - // We get all `doc(auto_cfg)`, `cfg` and `target_feature` attributes. - for attr in attrs { - if let Some(ident) = attr.ident() - && ident.name == sym::doc - && let Some(attrs) = attr.meta_item_list() - { - for attr in attrs.iter().filter(|attr| attr.has_name(sym::auto_cfg)) { - let MetaItemInner::MetaItem(attr) = attr else { - continue; - }; - match &attr.kind { - MetaItemKind::Word => { - if check_changed_auto_active_status( - &mut changed_auto_active_status, - attr, - cfg_info, - tcx, - true, - ) { - return None; - } - } - MetaItemKind::NameValue(lit) => { - if let LitKind::Bool(value) = lit.kind { - if check_changed_auto_active_status( - &mut changed_auto_active_status, - attr, - cfg_info, - tcx, - value, - ) { - return None; - } - } - } - MetaItemKind::List(sub_attrs) => { - if check_changed_auto_active_status( - &mut changed_auto_active_status, - attr, - cfg_info, - tcx, - true, - ) { - return None; - } - for sub_attr in sub_attrs.iter() { - if let Some(ident) = sub_attr.ident() - && (ident.name == sym::show || ident.name == sym::hide) - { - handle_auto_cfg_hide_show( - tcx, - cfg_info, - &sub_attr, - ident.name == sym::show, - &mut new_show_attrs, - &mut new_hide_attrs, - ); - } - } - } - } - } - } else if let hir::Attribute::Parsed(AttributeKind::TargetFeature { features, .. }) = attr { - // Treat `#[target_feature(enable = "feat")]` attributes as if they were - // `#[doc(cfg(target_feature = "feat"))]` attributes as well. - for (feature, _) in features { - cfg_info.current_cfg &= Cfg::Cfg(sym::target_feature, Some(*feature)); - } - continue; - } else if !cfg_info.parent_is_doc_cfg - && let Some(ident) = attr.ident() - && matches!(ident.name, sym::cfg | sym::cfg_trace) - && let Some(attr) = single(attr.meta_item_list()?) - && let Ok(new_cfg) = Cfg::parse(&attr) - { - cfg_info.current_cfg &= new_cfg; - } - } - - // If `doc(auto_cfg)` feature is disabled and `doc(cfg())` wasn't used, there is nothing - // to be done here. - if !cfg_info.auto_cfg_active && !cfg_info.parent_is_doc_cfg { - None - } else if cfg_info.parent_is_doc_cfg { - if cfg_info.current_cfg == Cfg::True { - None - } else { - Some(Arc::new(cfg_info.current_cfg.clone())) - } - } else { - // If `doc(auto_cfg)` feature is enabled, we want to collect all `cfg` items, we remove the - // hidden ones afterward. - match cfg_info.current_cfg.strip_hidden(&cfg_info.hidden_cfg) { - None | Some(Cfg::True) => None, - Some(cfg) => Some(Arc::new(cfg)), - } - } -} - pub(crate) trait NestedAttributesExt { /// Returns `true` if the attribute list contains a specific `word` fn has_word(self, word: Symbol) -> bool