From 63c50712f40055a60aefc8069661c3847bd27df4 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Fri, 22 Sep 2023 17:27:06 -0700 Subject: [PATCH 1/2] rustdoc-search: add support for associated types --- src/librustdoc/clean/types.rs | 44 ++ src/librustdoc/formats/cache.rs | 1 + src/librustdoc/formats/item_type.rs | 2 + src/librustdoc/html/render/mod.rs | 39 +- src/librustdoc/html/render/search_index.rs | 422 +++++++++++++++--- src/librustdoc/html/static/js/externs.js | 5 +- src/librustdoc/html/static/js/search.js | 331 ++++++++++++-- src/tools/rustdoc-js/tester.js | 29 +- tests/rustdoc-gui/search-tab.goml | 2 +- .../iterator-type-signatures.js | 29 ++ tests/rustdoc-js-std/parser-bindings.js | 245 ++++++++++ tests/rustdoc-js-std/parser-errors.js | 2 +- tests/rustdoc-js/assoc-type-backtrack.js | 163 +++++++ tests/rustdoc-js/assoc-type-backtrack.rs | 38 ++ tests/rustdoc-js/assoc-type.js | 45 ++ tests/rustdoc-js/assoc-type.rs | 12 + tests/rustdoc-js/gat.js | 57 +++ tests/rustdoc-js/gat.rs | 8 + tests/rustdoc-js/never-search.js | 10 + tests/rustdoc-js/trait-methods.js | 12 + tests/rustdoc-js/trait-methods.rs | 4 + 21 files changed, 1383 insertions(+), 117 deletions(-) create mode 100644 tests/rustdoc-js-std/iterator-type-signatures.js create mode 100644 tests/rustdoc-js-std/parser-bindings.js create mode 100644 tests/rustdoc-js/assoc-type-backtrack.js create mode 100644 tests/rustdoc-js/assoc-type-backtrack.rs create mode 100644 tests/rustdoc-js/assoc-type.js create mode 100644 tests/rustdoc-js/assoc-type.rs create mode 100644 tests/rustdoc-js/gat.js create mode 100644 tests/rustdoc-js/gat.rs create mode 100644 tests/rustdoc-js/trait-methods.js create mode 100644 tests/rustdoc-js/trait-methods.rs diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 871738cdc07d0..ded256fd75c52 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1651,6 +1651,13 @@ impl Type { } } + pub(crate) fn generic_args(&self) -> Option<&GenericArgs> { + match self { + Type::Path { path, .. } => path.generic_args(), + _ => None, + } + } + pub(crate) fn generics(&self) -> Option> { match self { Type::Path { path, .. } => path.generics(), @@ -2191,6 +2198,10 @@ impl Path { } } + pub(crate) fn generic_args(&self) -> Option<&GenericArgs> { + self.segments.last().map(|seg| &seg.args) + } + pub(crate) fn generics(&self) -> Option> { self.segments.last().and_then(|seg| { if let GenericArgs::AngleBracketed { ref args, .. } = seg.args { @@ -2232,6 +2243,39 @@ impl GenericArgs { GenericArgs::Parenthesized { inputs, output } => inputs.is_empty() && output.is_none(), } } + pub(crate) fn bindings<'a>(&'a self) -> Box + 'a> { + match self { + &GenericArgs::AngleBracketed { ref bindings, .. } => Box::new(bindings.iter().cloned()), + &GenericArgs::Parenthesized { ref output, .. } => Box::new( + output + .as_ref() + .map(|ty| TypeBinding { + assoc: PathSegment { + name: sym::Output, + args: GenericArgs::AngleBracketed { + args: Vec::new().into_boxed_slice(), + bindings: ThinVec::new(), + }, + }, + kind: TypeBindingKind::Equality { term: Term::Type((**ty).clone()) }, + }) + .into_iter(), + ), + } + } +} + +impl<'a> IntoIterator for &'a GenericArgs { + type IntoIter = Box + 'a>; + type Item = GenericArg; + fn into_iter(self) -> Self::IntoIter { + match self { + &GenericArgs::AngleBracketed { ref args, .. } => Box::new(args.iter().cloned()), + &GenericArgs::Parenthesized { ref inputs, .. } => { + Box::new(inputs.iter().cloned().map(GenericArg::Type)) + } + } + } } #[derive(Clone, PartialEq, Eq, Debug, Hash)] diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs index d63bbe5896e8f..9b1b6899751e2 100644 --- a/src/librustdoc/formats/cache.rs +++ b/src/librustdoc/formats/cache.rs @@ -369,6 +369,7 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> { &item, self.tcx, clean_impl_generics(self.cache.parent_stack.last()).as_ref(), + parent, self.cache, ), aliases: item.attrs.get_doc_aliases(), diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs index def3a90c8e812..22527876b76f9 100644 --- a/src/librustdoc/formats/item_type.rs +++ b/src/librustdoc/formats/item_type.rs @@ -49,6 +49,8 @@ pub(crate) enum ItemType { ProcAttribute = 23, ProcDerive = 24, TraitAlias = 25, + // This number is reserved for use in JavaScript + // Generic = 26, } impl Serialize for ItemType { diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 90691411f44a0..8b3cd9ca6fc0c 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -113,6 +113,7 @@ pub(crate) struct IndexItem { pub(crate) struct RenderType { id: Option, generics: Option>, + bindings: Option)>>, } impl Serialize for RenderType { @@ -129,10 +130,15 @@ impl Serialize for RenderType { Some(RenderTypeId::Index(idx)) => *idx, _ => panic!("must convert render types to indexes before serializing"), }; - if let Some(generics) = &self.generics { + if self.generics.is_some() || self.bindings.is_some() { let mut seq = serializer.serialize_seq(None)?; seq.serialize_element(&id)?; - seq.serialize_element(generics)?; + seq.serialize_element(self.generics.as_ref().map(Vec::as_slice).unwrap_or_default())?; + if self.bindings.is_some() { + seq.serialize_element( + self.bindings.as_ref().map(Vec::as_slice).unwrap_or_default(), + )?; + } seq.end() } else { id.serialize(serializer) @@ -140,13 +146,31 @@ impl Serialize for RenderType { } } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub(crate) enum RenderTypeId { DefId(DefId), Primitive(clean::PrimitiveType), + AssociatedType(Symbol), Index(isize), } +impl Serialize for RenderTypeId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let id = match &self { + // 0 is a sentinel, everything else is one-indexed + // concrete type + RenderTypeId::Index(idx) if *idx >= 0 => idx + 1, + // generic type parameter + RenderTypeId::Index(idx) => *idx, + _ => panic!("must convert render types to indexes before serializing"), + }; + id.serialize(serializer) + } +} + /// Full type of functions/methods in the search index. #[derive(Debug)] pub(crate) struct IndexItemFunctionType { @@ -171,17 +195,22 @@ impl Serialize for IndexItemFunctionType { } else { let mut seq = serializer.serialize_seq(None)?; match &self.inputs[..] { - [one] if one.generics.is_none() => seq.serialize_element(one)?, + [one] if one.generics.is_none() && one.bindings.is_none() => { + seq.serialize_element(one)? + } _ => seq.serialize_element(&self.inputs)?, } match &self.output[..] { [] if self.where_clause.is_empty() => {} - [one] if one.generics.is_none() => seq.serialize_element(one)?, + [one] if one.generics.is_none() && one.bindings.is_none() => { + seq.serialize_element(one)? + } _ => seq.serialize_element(&self.output)?, } for constraint in &self.where_clause { if let [one] = &constraint[..] && one.generics.is_none() + && one.bindings.is_none() { seq.serialize_element(one)?; } else { diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 1284f69e5d777..b3ae720fcf609 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -3,8 +3,10 @@ use std::collections::BTreeMap; use rustc_data_structures::fx::{FxHashMap, FxIndexMap}; use rustc_middle::ty::TyCtxt; +use rustc_span::def_id::DefId; use rustc_span::symbol::Symbol; use serde::ser::{Serialize, SerializeSeq, SerializeStruct, Serializer}; +use thin_vec::ThinVec; use crate::clean; use crate::clean::types::{Function, Generics, ItemId, Type, WherePredicate}; @@ -22,6 +24,7 @@ pub(crate) fn build_index<'tcx>( ) -> String { let mut itemid_to_pathid = FxHashMap::default(); let mut primitives = FxHashMap::default(); + let mut associated_types = FxHashMap::default(); let mut crate_paths = vec![]; // Attach all orphan items to the type's definition if the type @@ -38,7 +41,13 @@ pub(crate) fn build_index<'tcx>( parent: Some(parent), parent_idx: None, impl_id, - search_type: get_function_type_for_search(item, tcx, impl_generics.as_ref(), cache), + search_type: get_function_type_for_search( + item, + tcx, + impl_generics.as_ref(), + Some(parent), + cache, + ), aliases: item.attrs.get_doc_aliases(), deprecation: item.deprecation(tcx), }); @@ -76,83 +85,139 @@ pub(crate) fn build_index<'tcx>( let mut search_index = std::mem::replace(&mut cache.search_index, Vec::new()); for item in search_index.iter_mut() { fn insert_into_map( - ty: &mut RenderType, map: &mut FxHashMap, itemid: F, lastpathid: &mut isize, crate_paths: &mut Vec<(ItemType, Vec)>, item_type: ItemType, path: &[Symbol], - ) { + ) -> RenderTypeId { match map.entry(itemid) { - Entry::Occupied(entry) => ty.id = Some(RenderTypeId::Index(*entry.get())), + Entry::Occupied(entry) => RenderTypeId::Index(*entry.get()), Entry::Vacant(entry) => { let pathid = *lastpathid; entry.insert(pathid); *lastpathid += 1; crate_paths.push((item_type, path.to_vec())); - ty.id = Some(RenderTypeId::Index(pathid)); + RenderTypeId::Index(pathid) } } } - fn convert_render_type( - ty: &mut RenderType, + fn convert_render_type_id( + id: RenderTypeId, cache: &mut Cache, itemid_to_pathid: &mut FxHashMap, primitives: &mut FxHashMap, + associated_types: &mut FxHashMap, lastpathid: &mut isize, crate_paths: &mut Vec<(ItemType, Vec)>, - ) { - if let Some(generics) = &mut ty.generics { - for item in generics { - convert_render_type( - item, - cache, - itemid_to_pathid, - primitives, - lastpathid, - crate_paths, - ); - } - } + ) -> Option { let Cache { ref paths, ref external_paths, .. } = *cache; - let Some(id) = ty.id.clone() else { - assert!(ty.generics.is_some()); - return; - }; match id { RenderTypeId::DefId(defid) => { if let Some(&(ref fqp, item_type)) = paths.get(&defid).or_else(|| external_paths.get(&defid)) { - insert_into_map( - ty, + Some(insert_into_map( itemid_to_pathid, ItemId::DefId(defid), lastpathid, crate_paths, item_type, fqp, - ); + )) } else { - ty.id = None; + None } } RenderTypeId::Primitive(primitive) => { let sym = primitive.as_sym(); - insert_into_map( - ty, + Some(insert_into_map( primitives, sym, lastpathid, crate_paths, ItemType::Primitive, &[sym], + )) + } + RenderTypeId::Index(_) => Some(id), + RenderTypeId::AssociatedType(sym) => Some(insert_into_map( + associated_types, + sym, + lastpathid, + crate_paths, + ItemType::AssocType, + &[sym], + )), + } + } + + fn convert_render_type( + ty: &mut RenderType, + cache: &mut Cache, + itemid_to_pathid: &mut FxHashMap, + primitives: &mut FxHashMap, + associated_types: &mut FxHashMap, + lastpathid: &mut isize, + crate_paths: &mut Vec<(ItemType, Vec)>, + ) { + if let Some(generics) = &mut ty.generics { + for item in generics { + convert_render_type( + item, + cache, + itemid_to_pathid, + primitives, + associated_types, + lastpathid, + crate_paths, ); } - RenderTypeId::Index(_) => {} } + if let Some(bindings) = &mut ty.bindings { + bindings.retain_mut(|(associated_type, constraints)| { + let converted_associated_type = convert_render_type_id( + *associated_type, + cache, + itemid_to_pathid, + primitives, + associated_types, + lastpathid, + crate_paths, + ); + let Some(converted_associated_type) = converted_associated_type else { + return false; + }; + *associated_type = converted_associated_type; + for constraint in constraints { + convert_render_type( + constraint, + cache, + itemid_to_pathid, + primitives, + associated_types, + lastpathid, + crate_paths, + ); + } + true + }); + } + let Some(id) = ty.id.clone() else { + assert!(ty.generics.is_some()); + return; + }; + ty.id = convert_render_type_id( + id, + cache, + itemid_to_pathid, + primitives, + associated_types, + lastpathid, + crate_paths, + ); } if let Some(search_type) = &mut item.search_type { for item in &mut search_type.inputs { @@ -161,6 +226,7 @@ pub(crate) fn build_index<'tcx>( cache, &mut itemid_to_pathid, &mut primitives, + &mut associated_types, &mut lastpathid, &mut crate_paths, ); @@ -171,6 +237,7 @@ pub(crate) fn build_index<'tcx>( cache, &mut itemid_to_pathid, &mut primitives, + &mut associated_types, &mut lastpathid, &mut crate_paths, ); @@ -182,6 +249,7 @@ pub(crate) fn build_index<'tcx>( cache, &mut itemid_to_pathid, &mut primitives, + &mut associated_types, &mut lastpathid, &mut crate_paths, ); @@ -442,12 +510,39 @@ pub(crate) fn get_function_type_for_search<'tcx>( item: &clean::Item, tcx: TyCtxt<'tcx>, impl_generics: Option<&(clean::Type, clean::Generics)>, + parent: Option, cache: &Cache, ) -> Option { + let mut trait_info = None; + let impl_or_trait_generics = impl_generics.or_else(|| { + if let Some(def_id) = parent + && let Some(trait_) = cache.traits.get(&def_id) + && let Some((path, _)) = + cache.paths.get(&def_id).or_else(|| cache.external_paths.get(&def_id)) + { + let path = clean::Path { + res: rustc_hir::def::Res::Def(rustc_hir::def::DefKind::Trait, def_id), + segments: path + .iter() + .map(|name| clean::PathSegment { + name: *name, + args: clean::GenericArgs::AngleBracketed { + args: Vec::new().into_boxed_slice(), + bindings: ThinVec::new(), + }, + }) + .collect(), + }; + trait_info = Some((clean::Type::Path { path }, trait_.generics.clone())); + Some(trait_info.as_ref().unwrap()) + } else { + None + } + }); let (mut inputs, mut output, where_clause) = match *item.kind { - clean::FunctionItem(ref f) => get_fn_inputs_and_outputs(f, tcx, impl_generics, cache), - clean::MethodItem(ref m, _) => get_fn_inputs_and_outputs(m, tcx, impl_generics, cache), - clean::TyMethodItem(ref m) => get_fn_inputs_and_outputs(m, tcx, impl_generics, cache), + clean::FunctionItem(ref f) | clean::MethodItem(ref f, _) | clean::TyMethodItem(ref f) => { + get_fn_inputs_and_outputs(f, tcx, impl_or_trait_generics, cache) + } _ => return None, }; @@ -457,14 +552,23 @@ pub(crate) fn get_function_type_for_search<'tcx>( Some(IndexItemFunctionType { inputs, output, where_clause }) } -fn get_index_type(clean_type: &clean::Type, generics: Vec) -> RenderType { +fn get_index_type( + clean_type: &clean::Type, + generics: Vec, + rgen: &mut FxHashMap)>, +) -> RenderType { RenderType { - id: get_index_type_id(clean_type), + id: get_index_type_id(clean_type, rgen), generics: if generics.is_empty() { None } else { Some(generics) }, + bindings: None, } } -fn get_index_type_id(clean_type: &clean::Type) -> Option { +fn get_index_type_id( + clean_type: &clean::Type, + rgen: &mut FxHashMap)>, +) -> Option { + use rustc_hir::def::{DefKind, Res}; match *clean_type { clean::Type::Path { ref path, .. } => Some(RenderTypeId::DefId(path.def_id())), clean::DynTrait(ref bounds, _) => { @@ -472,18 +576,27 @@ fn get_index_type_id(clean_type: &clean::Type) -> Option { } clean::Primitive(p) => Some(RenderTypeId::Primitive(p)), clean::BorrowedRef { ref type_, .. } | clean::RawPointer(_, ref type_) => { - get_index_type_id(type_) + get_index_type_id(type_, rgen) } // The type parameters are converted to generics in `simplify_fn_type` clean::Slice(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Slice)), clean::Array(_, _) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Array)), clean::Tuple(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Tuple)), + clean::QPath(ref data) => { + if data.self_type.is_self_type() + && let Some(clean::Path { res: Res::Def(DefKind::Trait, trait_), .. }) = data.trait_ + { + let idx = -isize::try_from(rgen.len() + 1).unwrap(); + let (idx, _) = rgen + .entry(SimplifiedParam::AssociatedType(trait_, data.assoc.name)) + .or_insert_with(|| (idx, Vec::new())); + Some(RenderTypeId::Index(*idx)) + } else { + None + } + } // Not supported yet - clean::BareFunction(_) - | clean::Generic(_) - | clean::ImplTrait(_) - | clean::QPath { .. } - | clean::Infer => None, + clean::BareFunction(_) | clean::Generic(_) | clean::ImplTrait(_) | clean::Infer => None, } } @@ -493,6 +606,9 @@ enum SimplifiedParam { Symbol(Symbol), // every argument-position impl trait is its own type parameter Anonymous(isize), + // in a trait definition, the associated types are all bound to + // their own type parameter + AssociatedType(DefId, Symbol), } /// The point of this function is to lower generics and types into the simplified form that the @@ -523,10 +639,17 @@ fn simplify_fn_type<'tcx, 'a>( } // First, check if it's "Self". + let mut is_self = false; let mut arg = if let Some(self_) = self_ { match &*arg { - Type::BorrowedRef { type_, .. } if type_.is_self_type() => self_, - type_ if type_.is_self_type() => self_, + Type::BorrowedRef { type_, .. } if type_.is_self_type() => { + is_self = true; + self_ + } + type_ if type_.is_self_type() => { + is_self = true; + self_ + } arg => arg, } } else { @@ -585,11 +708,19 @@ fn simplify_fn_type<'tcx, 'a>( } } if let Some((idx, _)) = rgen.get(&SimplifiedParam::Symbol(arg_s)) { - res.push(RenderType { id: Some(RenderTypeId::Index(*idx)), generics: None }); + res.push(RenderType { + id: Some(RenderTypeId::Index(*idx)), + generics: None, + bindings: None, + }); } else { let idx = -isize::try_from(rgen.len() + 1).unwrap(); rgen.insert(SimplifiedParam::Symbol(arg_s), (idx, type_bounds)); - res.push(RenderType { id: Some(RenderTypeId::Index(idx)), generics: None }); + res.push(RenderType { + id: Some(RenderTypeId::Index(idx)), + generics: None, + bindings: None, + }); } } else if let Type::ImplTrait(ref bounds) = *arg { let mut type_bounds = Vec::new(); @@ -611,12 +742,16 @@ fn simplify_fn_type<'tcx, 'a>( } if is_return && !type_bounds.is_empty() { // In parameter position, `impl Trait` is a unique thing. - res.push(RenderType { id: None, generics: Some(type_bounds) }); + res.push(RenderType { id: None, generics: Some(type_bounds), bindings: None }); } else { // In parameter position, `impl Trait` is the same as an unnamed generic parameter. let idx = -isize::try_from(rgen.len() + 1).unwrap(); rgen.insert(SimplifiedParam::Anonymous(idx), (idx, type_bounds)); - res.push(RenderType { id: Some(RenderTypeId::Index(idx)), generics: None }); + res.push(RenderType { + id: Some(RenderTypeId::Index(idx)), + generics: None, + bindings: None, + }); } } else if let Type::Slice(ref ty) = *arg { let mut ty_generics = Vec::new(); @@ -631,7 +766,7 @@ fn simplify_fn_type<'tcx, 'a>( is_return, cache, ); - res.push(get_index_type(arg, ty_generics)); + res.push(get_index_type(arg, ty_generics, rgen)); } else if let Type::Array(ref ty, _) = *arg { let mut ty_generics = Vec::new(); simplify_fn_type( @@ -645,7 +780,7 @@ fn simplify_fn_type<'tcx, 'a>( is_return, cache, ); - res.push(get_index_type(arg, ty_generics)); + res.push(get_index_type(arg, ty_generics, rgen)); } else if let Type::Tuple(ref tys) = *arg { let mut ty_generics = Vec::new(); for ty in tys { @@ -661,7 +796,7 @@ fn simplify_fn_type<'tcx, 'a>( cache, ); } - res.push(get_index_type(arg, ty_generics)); + res.push(get_index_type(arg, ty_generics, rgen)); } else { // This is not a type parameter. So for example if we have `T, U: Option`, and we're // looking at `Option`, we enter this "else" condition, otherwise if it's `T`, we don't. @@ -669,12 +804,16 @@ fn simplify_fn_type<'tcx, 'a>( // So in here, we can add it directly and look for its own type parameters (so for `Option`, // we will look for them but not for `T`). let mut ty_generics = Vec::new(); - if let Some(arg_generics) = arg.generics() { - for gen in arg_generics.iter() { + let mut ty_bindings = Vec::new(); + if let Some(arg_generics) = arg.generic_args() { + for ty in arg_generics.into_iter().filter_map(|gen| match gen { + clean::GenericArg::Type(ty) => Some(ty), + _ => None, + }) { simplify_fn_type( self_, generics, - gen, + &ty, tcx, recurse + 1, &mut ty_generics, @@ -683,17 +822,180 @@ fn simplify_fn_type<'tcx, 'a>( cache, ); } + for binding in arg_generics.bindings() { + simplify_fn_binding( + self_, + generics, + &binding, + tcx, + recurse + 1, + &mut ty_bindings, + rgen, + is_return, + cache, + ); + } + } + // Every trait associated type on self gets assigned to a type parameter index + // this same one is used later for any appearances of these types + // + // for example, Iterator::next is: + // + // trait Iterator { + // fn next(&mut self) -> Option + // } + // + // Self is technically just Iterator, but we want to pretend it's more like this: + // + // fn next(self: Iterator) -> Option + if is_self + && let Type::Path { path } = arg + && let def_id = path.def_id() + && let Some(trait_) = cache.traits.get(&def_id) + && trait_.items.iter().any(|at| at.is_ty_associated_type()) + { + for assoc_ty in &trait_.items { + if let clean::ItemKind::TyAssocTypeItem(_generics, bounds) = &*assoc_ty.kind + && let Some(name) = assoc_ty.name + { + let idx = -isize::try_from(rgen.len() + 1).unwrap(); + let (idx, stored_bounds) = rgen + .entry(SimplifiedParam::AssociatedType(def_id, name)) + .or_insert_with(|| (idx, Vec::new())); + let idx = *idx; + if stored_bounds.is_empty() { + // Can't just pass stored_bounds to simplify_fn_type, + // because it also accepts rgen as a parameter. + // Instead, have it fill in this local, then copy it into the map afterward. + let mut type_bounds = Vec::new(); + for bound in bounds { + if let Some(path) = bound.get_trait_path() { + let ty = Type::Path { path }; + simplify_fn_type( + self_, + generics, + &ty, + tcx, + recurse + 1, + &mut type_bounds, + rgen, + is_return, + cache, + ); + } + } + let stored_bounds = &mut rgen + .get_mut(&SimplifiedParam::AssociatedType(def_id, name)) + .unwrap() + .1; + if stored_bounds.is_empty() { + *stored_bounds = type_bounds; + } + } + ty_bindings.push(( + RenderTypeId::AssociatedType(name), + vec![RenderType { + id: Some(RenderTypeId::Index(idx)), + generics: None, + bindings: None, + }], + )) + } + } } - let id = get_index_type_id(&arg); + let id = get_index_type_id(&arg, rgen); if id.is_some() || !ty_generics.is_empty() { res.push(RenderType { id, + bindings: if ty_bindings.is_empty() { None } else { Some(ty_bindings) }, generics: if ty_generics.is_empty() { None } else { Some(ty_generics) }, }); } } } +fn simplify_fn_binding<'tcx, 'a>( + self_: Option<&'a Type>, + generics: &Generics, + binding: &'a clean::TypeBinding, + tcx: TyCtxt<'tcx>, + recurse: usize, + res: &mut Vec<(RenderTypeId, Vec)>, + rgen: &mut FxHashMap)>, + is_return: bool, + cache: &Cache, +) { + let mut ty_binding_constraints = Vec::new(); + let ty_binding_assoc = RenderTypeId::AssociatedType(binding.assoc.name); + for gen in &binding.assoc.args { + match gen { + clean::GenericArg::Type(arg) => simplify_fn_type( + self_, + generics, + &arg, + tcx, + recurse + 1, + &mut ty_binding_constraints, + rgen, + is_return, + cache, + ), + clean::GenericArg::Lifetime(_) + | clean::GenericArg::Const(_) + | clean::GenericArg::Infer => {} + } + } + for binding in binding.assoc.args.bindings() { + simplify_fn_binding( + self_, + generics, + &binding, + tcx, + recurse + 1, + res, + rgen, + is_return, + cache, + ); + } + match &binding.kind { + clean::TypeBindingKind::Equality { term } => { + if let clean::Term::Type(arg) = &term { + simplify_fn_type( + self_, + generics, + arg, + tcx, + recurse + 1, + &mut ty_binding_constraints, + rgen, + is_return, + cache, + ); + } + } + clean::TypeBindingKind::Constraint { bounds } => { + for bound in &bounds[..] { + if let Some(path) = bound.get_trait_path() { + let ty = Type::Path { path }; + simplify_fn_type( + self_, + generics, + &ty, + tcx, + recurse + 1, + &mut ty_binding_constraints, + rgen, + is_return, + cache, + ); + } + } + } + } + res.push((ty_binding_assoc, ty_binding_constraints)); +} + /// Return the full list of types when bounds have been resolved. /// /// i.e. `fn foo>(x: u32, y: B)` will return @@ -701,13 +1003,15 @@ fn simplify_fn_type<'tcx, 'a>( fn get_fn_inputs_and_outputs<'tcx>( func: &Function, tcx: TyCtxt<'tcx>, - impl_generics: Option<&(clean::Type, clean::Generics)>, + impl_or_trait_generics: Option<&(clean::Type, clean::Generics)>, cache: &Cache, ) -> (Vec, Vec, Vec>) { let decl = &func.decl; + let mut rgen: FxHashMap)> = Default::default(); + let combined_generics; - let (self_, generics) = if let Some((impl_self, impl_generics)) = impl_generics { + let (self_, generics) = if let Some((impl_self, impl_generics)) = impl_or_trait_generics { match (impl_generics.is_empty(), func.generics.is_empty()) { (true, _) => (Some(impl_self), &func.generics), (_, true) => (Some(impl_self), impl_generics), @@ -729,8 +1033,6 @@ fn get_fn_inputs_and_outputs<'tcx>( (None, &func.generics) }; - let mut rgen: FxHashMap)> = Default::default(); - let mut arg_types = Vec::new(); for arg in decl.inputs.values.iter() { simplify_fn_type( diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index c7811b43d1641..2338931a18fd2 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -14,6 +14,7 @@ function initSearch(searchIndex){} * pathWithoutLast: Array, * pathLast: string, * generics: Array, + * bindings: Map<(string|integer), Array>, * }} */ let QueryElement; @@ -24,6 +25,7 @@ let QueryElement; * totalElems: number, * typeFilter: (null|string), * userQuery: string, + * isInBinding: (null|string), * }} */ let ParserState; @@ -191,8 +193,9 @@ let FunctionSearchType; /** * @typedef {{ * id: (null|number), - * ty: (null|number), + * ty: number, * generics: Array, + * bindings: Map>, * }} */ let FunctionType; diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index e9dd3c439b3f0..22dcb87014315 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -23,27 +23,27 @@ const itemTypes = [ "import", "struct", "enum", - "fn", + "fn", // 5 "type", "static", "trait", "impl", - "tymethod", + "tymethod", // 10 "method", "structfield", "variant", "macro", - "primitive", + "primitive", // 15 "associatedtype", "constant", "associatedconstant", "union", - "foreigntype", + "foreigntype", // 20 "keyword", "existential", "attr", "derive", - "traitalias", + "traitalias", // 25 "generic", ]; @@ -298,7 +298,7 @@ function initSearch(rawSearchIndex) { } function isEndCharacter(c) { - return ",>-]".indexOf(c) !== -1; + return "=,>-]".indexOf(c) !== -1; } function isStopCharacter(c) { @@ -398,7 +398,7 @@ function initSearch(rawSearchIndex) { * @return {boolean} */ function isSeparatorCharacter(c) { - return c === ","; + return c === "," || c === "="; } /** @@ -500,6 +500,8 @@ function initSearch(rawSearchIndex) { " does not accept generic parameters", ]; } + const bindingName = parserState.isInBinding; + parserState.isInBinding = null; return { name: "never", id: null, @@ -507,7 +509,9 @@ function initSearch(rawSearchIndex) { pathWithoutLast: [], pathLast: "never", generics: [], + bindings: new Map(), typeFilter: "primitive", + bindingName, }; } if (path.startsWith("::")) { @@ -542,14 +546,27 @@ function initSearch(rawSearchIndex) { if (isInGenerics) { parserState.genericsElems += 1; } + const bindingName = parserState.isInBinding; + parserState.isInBinding = null; + const bindings = new Map(); return { name: name.trim(), id: null, fullPath: pathSegments, pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), pathLast: pathSegments[pathSegments.length - 1], - generics: generics, + generics: generics.filter(gen => { + // Syntactically, bindings are parsed as generics, + // but the query engine treats them differently. + if (gen.bindingName !== null) { + bindings.set(gen.bindingName.name, [gen, ...gen.bindingName.generics]); + return false; + } + return true; + }), + bindings, typeFilter, + bindingName, }; } @@ -608,6 +625,7 @@ function initSearch(rawSearchIndex) { } } else if ( c === "[" || + c === "=" || isStopCharacter(c) || isSpecialStartCharacter(c) || isSeparatorCharacter(c) @@ -657,6 +675,7 @@ function initSearch(rawSearchIndex) { parserState.pos += 1; getItemsBefore(query, parserState, generics, "]"); const typeFilter = parserState.typeFilter; + const isInBinding = parserState.isInBinding; if (typeFilter !== null && typeFilter !== "primitive") { throw [ "Invalid search type: primitive ", @@ -667,10 +686,16 @@ function initSearch(rawSearchIndex) { ]; } parserState.typeFilter = null; + parserState.isInBinding = null; parserState.totalElems += 1; if (isInGenerics) { parserState.genericsElems += 1; } + for (const gen of generics) { + if (gen.bindingName !== null) { + throw ["Type parameter ", "=", " cannot be within slice ", "[]"]; + } + } elems.push({ name: "[]", id: null, @@ -679,6 +704,8 @@ function initSearch(rawSearchIndex) { pathLast: "[]", generics, typeFilter: "primitive", + bindingName: isInBinding, + bindings: new Map(), }); } else { const isStringElem = parserState.userQuery[start] === "\""; @@ -705,15 +732,38 @@ function initSearch(rawSearchIndex) { if (start >= end && generics.length === 0) { return; } - elems.push( - createQueryElement( - query, - parserState, - parserState.userQuery.slice(start, end), - generics, - isInGenerics - ) - ); + if (parserState.userQuery[parserState.pos] === "=") { + if (parserState.isInBinding) { + throw ["Cannot write ", "=", " twice in a binding"]; + } + if (!isInGenerics) { + throw ["Type parameter ", "=", " must be within generics list"]; + } + const name = parserState.userQuery.slice(start, end).trim(); + if (name === "!") { + throw ["Type parameter ", "=", " key cannot be ", "!", " never type"]; + } + if (name.includes("!")) { + throw ["Type parameter ", "=", " key cannot be ", "!", " macro"]; + } + if (name.includes("::")) { + throw ["Type parameter ", "=", " key cannot contain ", "::", " path"]; + } + if (name.includes(":")) { + throw ["Type parameter ", "=", " key cannot contain ", ":", " type"]; + } + parserState.isInBinding = { name, generics }; + } else { + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics + ) + ); + } } } @@ -737,6 +787,8 @@ function initSearch(rawSearchIndex) { // If this is a generic, keep the outer item's type filter around. const oldTypeFilter = parserState.typeFilter; parserState.typeFilter = null; + const oldIsInBinding = parserState.isInBinding; + parserState.isInBinding = null; let extra = ""; if (endChar === ">") { @@ -752,6 +804,9 @@ function initSearch(rawSearchIndex) { while (parserState.pos < parserState.length) { const c = parserState.userQuery[parserState.pos]; if (c === endChar) { + if (parserState.isInBinding) { + throw ["Unexpected ", endChar, " after ", "="]; + } break; } else if (isSeparatorCharacter(c)) { parserState.pos += 1; @@ -791,7 +846,9 @@ function initSearch(rawSearchIndex) { throw [ "Expected ", ",", - " or ", + ", ", + "=", + ", or ", endChar, ...extra, ", found ", @@ -801,6 +858,8 @@ function initSearch(rawSearchIndex) { throw [ "Expected ", ",", + " or ", + "=", ...extra, ", found ", c, @@ -828,6 +887,7 @@ function initSearch(rawSearchIndex) { parserState.pos += 1; parserState.typeFilter = oldTypeFilter; + parserState.isInBinding = oldIsInBinding; } /** @@ -1054,6 +1114,11 @@ function initSearch(rawSearchIndex) { for (const elem2 of elem.generics) { convertTypeFilterOnElem(elem2); } + for (const constraints of elem.bindings.values()) { + for (const constraint of constraints) { + convertTypeFilterOnElem(constraint); + } + } } userQuery = userQuery.trim(); const parserState = { @@ -1063,6 +1128,7 @@ function initSearch(rawSearchIndex) { totalElems: 0, genericsElems: 0, typeFilter: null, + isInBinding: null, userQuery: userQuery.toLowerCase(), }; let query = newParsedQuery(userQuery); @@ -1342,7 +1408,8 @@ function initSearch(rawSearchIndex) { const fl = fnTypesIn.length; // One element fast path / base case - if (ql === 1 && queryElems[0].generics.length === 0) { + if (ql === 1 && queryElems[0].generics.length === 0 + && queryElems[0].bindings.size === 0) { const queryElem = queryElems[0]; for (const fnType of fnTypesIn) { if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) { @@ -1453,16 +1520,33 @@ function initSearch(rawSearchIndex) { whereClause, mgensScratch, mgensScratch => { - if (fnType.generics.length === 0 && queryElem.generics.length === 0) { + if (fnType.generics.length === 0 && queryElem.generics.length === 0 + && fnType.bindings.size === 0 && queryElem.bindings.size === 0) { return !solutionCb || solutionCb(mgensScratch); } - return unifyFunctionTypes( - fnType.generics, - queryElem.generics, + const solution = unifyFunctionTypeCheckBindings( + fnType, + queryElem, whereClause, - mgensScratch, - solutionCb + mgensScratch ); + if (!solution) { + return false; + } + const simplifiedGenerics = solution.simplifiedGenerics; + for (const simplifiedMgens of solution.mgens) { + const passesUnification = unifyFunctionTypes( + simplifiedGenerics, + queryElem.generics, + whereClause, + simplifiedMgens, + solutionCb + ); + if (passesUnification) { + return true; + } + } + return false; } ); if (passesUnification) { @@ -1491,8 +1575,11 @@ function initSearch(rawSearchIndex) { const generics = fnType.id < 0 ? whereClause[(-fnType.id) - 1] : fnType.generics; + const bindings = fnType.bindings ? + Array.from(fnType.bindings.values()).flat() : + []; const passesUnification = unifyFunctionTypes( - fnTypes.toSpliced(i, 1, ...generics), + fnTypes.toSpliced(i, 1, ...generics, ...bindings), queryElems, whereClause, mgensScratch, @@ -1504,22 +1591,37 @@ function initSearch(rawSearchIndex) { } return false; } - function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens) { + /** + * Check if this function is a match candidate. + * + * This function is all the fast checks that don't require backtracking. + * It checks that two items are not named differently, and is load-bearing for that. + * It also checks that, if the query has generics, the function type must have generics + * or associated type bindings: that's not load-bearing, but it prevents unnecessary + * backtracking later. + * + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {[FunctionSearchType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensIn - Map functions generics to query generics. + * @returns {boolean} + */ + function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgensIn) { // type filters look like `trait:Read` or `enum:Result` if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { return false; } // fnType.id < 0 means generic // queryElem.id < 0 does too - // mgens[fnType.id] = queryElem.id - // or, if mgens[fnType.id] = 0, then we've matched this generic with a bare trait + // mgensIn[fnType.id] = queryElem.id + // or, if mgensIn[fnType.id] = 0, then we've matched this generic with a bare trait // and should make that same decision everywhere it appears if (fnType.id < 0 && queryElem.id < 0) { - if (mgens !== null) { - if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) { + if (mgensIn) { + if (mgensIn.has(fnType.id) && mgensIn.get(fnType.id) !== queryElem.id) { return false; } - for (const [fid, qid] of mgens.entries()) { + for (const [fid, qid] of mgensIn.entries()) { if (fnType.id !== fid && queryElem.id === qid) { return false; } @@ -1528,6 +1630,7 @@ function initSearch(rawSearchIndex) { } } } + return true; } else { if (queryElem.id === typeNameIdOfArrayOrSlice && (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) @@ -1539,7 +1642,12 @@ function initSearch(rawSearchIndex) { } // If the query elem has generics, and the function doesn't, // it can't match. - if (fnType.generics.length === 0 && queryElem.generics.length !== 0) { + if ((fnType.generics.length + fnType.bindings.size) === 0 && + queryElem.generics.length !== 0 + ) { + return false; + } + if (fnType.bindings.size < queryElem.bindings.size) { return false; } // If the query element is a path (it contains `::`), we need to check if this @@ -1568,9 +1676,87 @@ function initSearch(rawSearchIndex) { return false; } } + return true; } - return true; } + /** + * This function checks the associated type bindings. Any that aren't matched get converted + * to generics, and this function returns an array of the function's generics with these + * simplified bindings added to them. That is, it takes a path like this: + * + * Iterator + * + * ... if queryElem itself has an `Item=` in it, then this function returns an empty array. + * But if queryElem contains no Item=, then this function returns a one-item array with the + * ID of u32 in it, and the rest of the matching engine acts as if `Iterator` were + * the type instead. + * + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map} mgensIn - Map functions generics to query generics. + * Never modified. + * @returns {false|{mgens: [Map], simplifiedGenerics: [FunctionType]}} + */ + function unifyFunctionTypeCheckBindings(fnType, queryElem, whereClause, mgensIn) { + if (fnType.bindings.size < queryElem.bindings.size) { + return false; + } + let simplifiedGenerics = fnType.generics || []; + if (fnType.bindings.size > 0) { + let mgensSolutionSet = [mgensIn]; + for (const [name, constraints] of queryElem.bindings.entries()) { + if (mgensSolutionSet.length === 0) { + return false; + } + if (!fnType.bindings.has(name)) { + return false; + } + const fnTypeBindings = fnType.bindings.get(name); + mgensSolutionSet = mgensSolutionSet.flatMap(mgens => { + const newSolutions = []; + unifyFunctionTypes( + fnTypeBindings, + constraints, + whereClause, + mgens, + newMgens => { + newSolutions.push(newMgens); + // return `false` makes unifyFunctionTypes return the full set of + // possible solutions + return false; + } + ); + return newSolutions; + }); + } + if (mgensSolutionSet.length === 0) { + return false; + } + const binds = Array.from(fnType.bindings.entries()).flatMap(entry => { + const [name, constraints] = entry; + if (queryElem.bindings.has(name)) { + return []; + } else { + return constraints; + } + }); + if (simplifiedGenerics.length > 0) { + simplifiedGenerics = [...simplifiedGenerics, ...binds]; + } else { + simplifiedGenerics = binds; + } + return { simplifiedGenerics, mgens: mgensSolutionSet }; + } + return { simplifiedGenerics, mgens: [mgensIn] }; + } + /** + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgens - Map functions generics to query generics. + * @returns {boolean} + */ function unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens) { if (fnType.id < 0 && queryElem.id >= 0) { if (!whereClause) { @@ -1578,7 +1764,7 @@ function initSearch(rawSearchIndex) { } // mgens[fnType.id] === 0 indicates that we committed to unboxing this generic // mgens[fnType.id] === null indicates that we haven't decided yet - if (mgens !== null && mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { + if (mgens && mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { return false; } // This is only a potential unbox if the search query appears in the where clause @@ -1586,8 +1772,12 @@ function initSearch(rawSearchIndex) { // `fn read_all(R) -> Result` // generic `R` is considered "unboxed" return checkIfInList(whereClause[(-fnType.id) - 1], queryElem, whereClause); - } else if (fnType.generics && fnType.generics.length > 0) { - return checkIfInList(fnType.generics, queryElem, whereClause); + } else if (fnType.generics.length > 0 || fnType.bindings.size > 0) { + const simplifiedGenerics = [ + ...fnType.generics, + ...Array.from(fnType.bindings.values()).flat(), + ]; + return checkIfInList(simplifiedGenerics, queryElem, whereClause); } return false; } @@ -1622,15 +1812,17 @@ function initSearch(rawSearchIndex) { * @return {boolean} - Returns true if the type matches, false otherwise. */ function checkType(row, elem, whereClause) { - if (elem.id < 0) { - return row.id < 0 || checkIfInList(row.generics, elem, whereClause); - } - if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 && - typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 && - // special case - elem.id !== typeNameIdOfArrayOrSlice - ) { - return row.id === elem.id || checkIfInList(row.generics, elem, whereClause); + if (row.bindings.size === 0 && elem.bindings.size === 0) { + if (elem.id < 0) { + return row.id < 0 || checkIfInList(row.generics, elem, whereClause); + } + if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 && + typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 && + // special case + elem.id !== typeNameIdOfArrayOrSlice + ) { + return row.id === elem.id || checkIfInList(row.generics, elem, whereClause); + } } return unifyFunctionTypes([row], [elem], whereClause); } @@ -1977,7 +2169,7 @@ function initSearch(rawSearchIndex) { elem.id = match; } if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 - && elem.generics.length === 0) + && elem.generics.length === 0 && elem.bindings.size === 0) || elem.typeFilter === TY_GENERIC) { if (genericSymbols.has(elem.name)) { elem.id = genericSymbols.get(elem.name); @@ -2020,6 +2212,23 @@ function initSearch(rawSearchIndex) { for (const elem2 of elem.generics) { convertNameToId(elem2); } + elem.bindings = new Map(Array.from(elem.bindings.entries()) + .map(entry => { + const [name, constraints] = entry; + if (!typeNameIdMap.has(name)) { + parsedQuery.error = [ + "Type parameter ", + name, + " does not exist", + ]; + } + for (const elem2 of constraints) { + convertNameToId(elem2); + } + + return [typeNameIdMap.get(name), constraints]; + }) + ); } for (const elem of parsedQuery.elems) { @@ -2536,16 +2745,39 @@ ${item.displayPath}${name}\ function buildItemSearchType(type, lowercasePaths) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; - let pathIndex, generics; + const BINDINGS_DATA = 2; + let pathIndex, generics, bindings; if (typeof type === "number") { pathIndex = type; generics = []; + bindings = new Map(); } else { pathIndex = type[PATH_INDEX_DATA]; generics = buildItemSearchTypeAll( type[GENERICS_DATA], lowercasePaths ); + if (type.length > BINDINGS_DATA) { + bindings = new Map(type[BINDINGS_DATA].map(binding => { + const [assocType, constraints] = binding; + // Associated type constructors are represented sloppily in rustdoc's + // type search, to make the engine simpler. + // + // MyType=Result> is equivalent to MyType>=T> + // and both are, essentially + // MyType)>, except the tuple isn't actually there. + // It's more like the value of a type binding is naturally an array, + // which rustdoc calls "constraints". + // + // As a result, the key should never have generics on it. + return [ + buildItemSearchType(assocType, lowercasePaths).id, + buildItemSearchTypeAll(constraints, lowercasePaths), + ]; + })); + } else { + bindings = new Map(); + } } if (pathIndex < 0) { // types less than 0 are generic parameters @@ -2555,6 +2787,7 @@ ${item.displayPath}${name}\ ty: TY_GENERIC, path: null, generics, + bindings, }; } if (pathIndex === 0) { @@ -2564,6 +2797,7 @@ ${item.displayPath}${name}\ ty: null, path: null, generics, + bindings, }; } const item = lowercasePaths[pathIndex - 1]; @@ -2572,6 +2806,7 @@ ${item.displayPath}${name}\ ty: item.ty, path: item.path, generics, + bindings, }; } diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index c7e6dd3615e94..6e630a80454ad 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -122,7 +122,31 @@ function checkNeededFields(fullPath, expected, error_text, queryName, position) } function valueCheck(fullPath, expected, result, error_text, queryName) { - if (Array.isArray(expected)) { + if (Array.isArray(expected) && result instanceof Map) { + const expected_set = new Set(); + for (const [key, expected_value] of expected) { + expected_set.add(key); + checkNeededFields(fullPath, expected_value, error_text, queryName, key); + if (result.has(key)) { + valueCheck( + fullPath + "[" + key + "]", + expected_value, + result.get(key), + error_text, + queryName + ); + } else { + error_text.push(`${queryName}==> EXPECTED has extra key in map from field ` + + `\`${fullPath}\` (key ${key}): \`${JSON.stringify(expected_value)}\``); + } + } + for (const [key, result_value] of result.entries()) { + if (!expected_set.has(key)) { + error_text.push(`${queryName}==> EXPECTED missing key in map from field ` + + `\`${fullPath}\` (key ${key}): \`${JSON.stringify(result_value)}\``); + } + } + } else if (Array.isArray(expected)) { let i; for (i = 0; i < expected.length; ++i) { checkNeededFields(fullPath, expected[i], error_text, queryName, i); @@ -153,6 +177,9 @@ function valueCheck(fullPath, expected, result, error_text, queryName) { } let result_v = result[key]; if (result_v !== null && key === "error") { + if (!result_v.forEach) { + throw result_v; + } result_v.forEach((value, index) => { value = value.split(" ").join(" "); if (index % 2 === 1) { diff --git a/tests/rustdoc-gui/search-tab.goml b/tests/rustdoc-gui/search-tab.goml index db1605ff220a1..b52bb0688c1b5 100644 --- a/tests/rustdoc-gui/search-tab.goml +++ b/tests/rustdoc-gui/search-tab.goml @@ -80,7 +80,7 @@ set-window-size: (851, 600) // Check the size and count in tabs assert-text: ("#search-tabs > button:nth-child(1) > .count", " (25) ") -assert-text: ("#search-tabs > button:nth-child(2) > .count", " (5)  ") +assert-text: ("#search-tabs > button:nth-child(2) > .count", " (6)  ") assert-text: ("#search-tabs > button:nth-child(3) > .count", " (0)  ") store-property: ("#search-tabs > button:nth-child(1)", {"offsetWidth": buttonWidth}) assert-property: ("#search-tabs > button:nth-child(2)", {"offsetWidth": |buttonWidth|}) diff --git a/tests/rustdoc-js-std/iterator-type-signatures.js b/tests/rustdoc-js-std/iterator-type-signatures.js new file mode 100644 index 0000000000000..c18ffc1651c15 --- /dev/null +++ b/tests/rustdoc-js-std/iterator-type-signatures.js @@ -0,0 +1,29 @@ +// ignore-order + +const FILTER_CRATE = "std"; + +const EXPECTED = [ + { + 'query': 'iterator -> option', + 'others': [ + { 'path': 'std::iter::Iterator', 'name': 'max' }, + { 'path': 'std::iter::Iterator', 'name': 'min' }, + { 'path': 'std::iter::Iterator', 'name': 'last' }, + { 'path': 'std::iter::Iterator', 'name': 'next' }, + ], + }, + { + 'query': 'iterator, usize -> option', + 'others': [ + { 'path': 'std::iter::Iterator', 'name': 'nth' }, + ], + }, + { + // Something should be done so that intoiterator is considered a match + // for plain iterator. + 'query': 'iterator, intoiterator -> ordering', + 'others': [ + { 'path': 'std::iter::Iterator', 'name': 'cmp' }, + ], + }, +]; diff --git a/tests/rustdoc-js-std/parser-bindings.js b/tests/rustdoc-js-std/parser-bindings.js new file mode 100644 index 0000000000000..faf880c8116c4 --- /dev/null +++ b/tests/rustdoc-js-std/parser-bindings.js @@ -0,0 +1,245 @@ +const PARSED = [ + { + query: 'A', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [ + { + name: "c", + fullPath: ["c"], + pathWithoutLast: [], + pathLast: "c", + generics: [], + typeFilter: -1, + }, + ] + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A', + returned: [], + userQuery: 'a', + error: null, + }, + { + query: 'A', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [{ + name: "c", + fullPath: ["c"], + pathWithoutLast: [], + pathLast: "c", + generics: [], + typeFilter: -1, + }] + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A', + returned: [], + userQuery: 'a', + error: null, + }, + { + query: 'A', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [{ + name: "never", + fullPath: ["never"], + pathWithoutLast: [], + pathLast: "never", + generics: [], + typeFilter: 15, + }] + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A', + returned: [], + userQuery: 'a', + error: null, + }, + { + query: 'A', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [{ + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [], + typeFilter: 15, + }] + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A', + returned: [], + userQuery: 'a', + error: null, + }, + { + query: 'A', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [{ + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "never", + fullPath: ["never"], + pathWithoutLast: [], + pathLast: "never", + generics: [], + typeFilter: 15, + }, + ], + typeFilter: 15, + }] + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A', + returned: [], + userQuery: 'a', + error: null, + }, + { + query: 'A', + elems: [], + foundElems: 0, + original: 'A', + returned: [], + userQuery: 'a', + error: "Cannot write `=` twice in a binding", + }, + { + query: 'A', + elems: [], + foundElems: 0, + original: 'A', + returned: [], + userQuery: 'a', + error: "Unexpected `>` after `=`", + }, + { + query: 'B=C', + elems: [], + foundElems: 0, + original: 'B=C', + returned: [], + userQuery: 'b=c', + error: "Type parameter `=` must be within generics list", + }, + { + query: '[B=C]', + elems: [], + foundElems: 0, + original: '[B=C]', + returned: [], + userQuery: '[b=c]', + error: "Type parameter `=` cannot be within slice `[]`", + }, + { + query: 'A=C>', + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + bindings: [ + [ + 'b', + [ + { + name: "c", + fullPath: ["c"], + pathWithoutLast: [], + pathLast: "c", + generics: [], + typeFilter: -1, + }, + { + name: "x", + fullPath: ["x"], + pathWithoutLast: [], + pathLast: "x", + generics: [], + typeFilter: -1, + }, + ], + ], + ], + typeFilter: -1, + }, + ], + foundElems: 1, + original: 'A=C>', + returned: [], + userQuery: 'a=c>', + error: null, + }, +]; diff --git a/tests/rustdoc-js-std/parser-errors.js b/tests/rustdoc-js-std/parser-errors.js index b32bfea5439aa..ab8d72bf71b13 100644 --- a/tests/rustdoc-js-std/parser-errors.js +++ b/tests/rustdoc-js-std/parser-errors.js @@ -303,7 +303,7 @@ const PARSED = [ original: '->a<>b', returned: [], userQuery: '->a<>b', - error: 'Expected `,` after `>`, found `b`', + error: 'Expected `,` or `=` after `>`, found `b`', }, { query: "a<->", diff --git a/tests/rustdoc-js/assoc-type-backtrack.js b/tests/rustdoc-js/assoc-type-backtrack.js new file mode 100644 index 0000000000000..493e1a9910d18 --- /dev/null +++ b/tests/rustdoc-js/assoc-type-backtrack.js @@ -0,0 +1,163 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'mytrait, mytrait2 -> T', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyTrait', 'name': 'fold' }, + { 'path': 'assoc_type_backtrack::Cloned', 'name': 'fold' }, + ], + }, + { + 'query': 'mytrait, mytrait2 -> T', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyTrait', 'name': 'fold' }, + { 'path': 'assoc_type_backtrack::Cloned', 'name': 'fold' }, + ], + }, + { + 'query': 'mytrait, mytrait2 -> T', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyTrait', 'name': 'fold' }, + { 'path': 'assoc_type_backtrack::Cloned', 'name': 'fold' }, + ], + }, + { + 'query': 'mytrait, mytrait2 -> T', + 'correction': null, + 'others': [], + }, + { + 'query': 'mytrait, mytrait2 -> T', + 'correction': null, + 'others': [], + }, + { + 'query': 'mytrait -> Option', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyTrait', 'name': 'next' }, + ], + }, + { + 'query': 'mytrait -> Option', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyTrait', 'name': 'next' }, + ], + }, + { + 'query': 'mytrait -> Option', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::Cloned', 'name': 'next' }, + ], + }, + { + 'query': 'mytrait -> Option', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::Cloned', 'name': 'next' }, + ], + }, + // The first two define the base case. + { + 'query': 'myintofuture> -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future' }, + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + { + 'query': 'myintofuture>, myintofuture> -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + // Unboxings of the one-argument case. + { + 'query': 'myfuture -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future' }, + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + { + 'query': 'myintofuture> -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future' }, + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + // Invalid unboxing of the one-argument case. + // If you unbox one of the myfutures, you need to unbox both of them. + { + 'query': 'myintofuture -> myfuture', + 'correction': null, + 'others': [], + }, + // Unboxings of the two-argument case. + { + 'query': 'myintofuture, myintofuture -> t', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + { + 'query': 'myintofuture, myintofuture -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + { + 'query': 'myintofuture, myintofuture -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + { + 'query': 'myfuture, myfuture -> myfuture', + 'correction': null, + 'others': [ + { 'path': 'assoc_type_backtrack::MyIntoFuture', 'name': 'into_future_2' }, + ], + }, + // Invalid unboxings of the two-argument case. + // If you unbox one of the myfutures, you need to unbox all of them. + { + 'query': 'myintofuture, myintofuture> -> myfuture', + 'correction': null, + 'others': [], + }, + { + 'query': 'myintofuture>, myintofuture -> myfuture', + 'correction': null, + 'others': [], + }, + { + 'query': 'myintofuture>, myintofuture> -> t', + 'correction': null, + 'others': [], + }, + // different generics don't match up either + { + 'query': 'myintofuture>, myintofuture> -> myfuture', + 'correction': null, + 'others': [], + }, + { + 'query': 'myintofuture -> myfuture', + 'correction': null, + 'others': [], + }, +]; diff --git a/tests/rustdoc-js/assoc-type-backtrack.rs b/tests/rustdoc-js/assoc-type-backtrack.rs new file mode 100644 index 0000000000000..c3cdd78c6e1c4 --- /dev/null +++ b/tests/rustdoc-js/assoc-type-backtrack.rs @@ -0,0 +1,38 @@ +pub trait MyTrait2 { + type Output; +} + +pub trait MyTrait { + type Item; + fn next(&mut self) -> Option; + fn fold(self, init: B, f: F) -> B where + Self: Sized, + F: MyTrait2<(B, Self::Item), Output=B>; +} + +pub struct Cloned(I); + +impl<'a, T, I> MyTrait for Cloned where + T: 'a + Clone, + I: MyTrait +{ + type Item = T; + fn next(&mut self) -> Option { loop {} } + fn fold(self, init: B, f: F) -> B where + Self: Sized, + F: MyTrait2<(B, Self::Item), Output=B> + { + loop {} + } +} + +pub trait MyFuture { + type Output; +} + +pub trait MyIntoFuture { + type Output; + type Fut: MyFuture; + fn into_future(self) -> Self::Fut; + fn into_future_2(self, other: Self) -> Self::Fut; +} diff --git a/tests/rustdoc-js/assoc-type.js b/tests/rustdoc-js/assoc-type.js new file mode 100644 index 0000000000000..cc3afaa17c07d --- /dev/null +++ b/tests/rustdoc-js/assoc-type.js @@ -0,0 +1,45 @@ +// exact-check + +const EXPECTED = [ + // if I just use generics, then the generics version + // and the type binding version both show up + { + 'query': 'iterator -> u32', + 'correction': null, + 'others': [ + { 'path': 'assoc_type', 'name': 'my_fn' }, + { 'path': 'assoc_type::my', 'name': 'other_fn' }, + ], + }, + { + 'query': 'iterator', + 'correction': null, + 'in_args': [ + { 'path': 'assoc_type', 'name': 'my_fn' }, + { 'path': 'assoc_type::my', 'name': 'other_fn' }, + ], + }, + // if I write an explicit binding, only it shows up + { + 'query': 'iterator -> u32', + 'correction': null, + 'others': [ + { 'path': 'assoc_type', 'name': 'my_fn' }, + ], + }, + // case insensitivity + { + 'query': 'iterator -> u32', + 'correction': null, + 'others': [ + { 'path': 'assoc_type', 'name': 'my_fn' }, + ], + }, + // wrong binding name, no result + { + 'query': 'iterator -> u32', + 'correction': null, + 'in_args': [], + 'others': [], + }, +]; diff --git a/tests/rustdoc-js/assoc-type.rs b/tests/rustdoc-js/assoc-type.rs new file mode 100644 index 0000000000000..e12e73cb546df --- /dev/null +++ b/tests/rustdoc-js/assoc-type.rs @@ -0,0 +1,12 @@ +pub fn my_fn>(_x: X) -> u32 { + 3 +} + +pub struct Something; + +pub mod my { + pub trait Iterator {} + pub fn other_fn>(_: X) -> u32 { + 3 + } +} diff --git a/tests/rustdoc-js/gat.js b/tests/rustdoc-js/gat.js new file mode 100644 index 0000000000000..7cb6a85d135f9 --- /dev/null +++ b/tests/rustdoc-js/gat.js @@ -0,0 +1,57 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'foo=u8> -> u32', + 'correction': null, + 'in_args': [], + 'others': [ + { 'path': 'gat', 'name': 'sample' }, + ], + }, + { + 'query': 'foo=u8> -> !', + 'correction': null, + 'in_args': [], + 'others': [ + { 'path': 'gat', 'name': 'synergy' }, + ], + }, + { + 'query': 'foo=u8>', + 'correction': null, + 'in_args': [ + { 'path': 'gat', 'name': 'sample' }, + { 'path': 'gat', 'name': 'synergy' }, + ], + }, + { + 'query': 'foo=u32>', + 'correction': null, + 'in_args': [ + { 'path': 'gat', 'name': 'consider' }, + ], + }, + { + // This one is arguably a bug, because the way rustdoc + // stores GATs in the search index is sloppy, but it's + // precise enough to match most of the samples in the + // GAT initiative repo + 'query': 'foo=u8>', + 'correction': null, + 'in_args': [ + { 'path': 'gat', 'name': 'consider' }, + ], + }, + { + // This one is arguably a bug, because the way rustdoc + // stores GATs in the search index is sloppy, but it's + // precise enough to match most of the samples in the + // GAT initiative repo + 'query': 'foo=T>', + 'correction': null, + 'in_args': [ + { 'path': 'gat', 'name': 'integrate' }, + ], + }, +]; diff --git a/tests/rustdoc-js/gat.rs b/tests/rustdoc-js/gat.rs new file mode 100644 index 0000000000000..b4861cc683ff3 --- /dev/null +++ b/tests/rustdoc-js/gat.rs @@ -0,0 +1,8 @@ +pub trait Foo { + type Assoc; +} + +pub fn sample = u8>>(_: X) -> u32 { loop {} } +pub fn synergy(_: impl Foo = u8>) -> ! { loop {} } +pub fn consider(_: impl Foo = u32>) -> bool { loop {} } +pub fn integrate(_: impl Foo = T>) -> T { loop {} } diff --git a/tests/rustdoc-js/never-search.js b/tests/rustdoc-js/never-search.js index ed24d693133ba..9f18370c2f511 100644 --- a/tests/rustdoc-js/never-search.js +++ b/tests/rustdoc-js/never-search.js @@ -43,4 +43,14 @@ const EXPECTED = [ { 'path': 'never_search', 'name': 'box_uninteresting' }, ], }, + { + 'query': 'box', + 'in_args': [], + 'returned': [], + }, + { + 'query': 'box', + 'in_args': [], + 'returned': [], + }, ]; diff --git a/tests/rustdoc-js/trait-methods.js b/tests/rustdoc-js/trait-methods.js new file mode 100644 index 0000000000000..dafad5e43784f --- /dev/null +++ b/tests/rustdoc-js/trait-methods.js @@ -0,0 +1,12 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'mytrait -> option', + 'correction': null, + 'in_args': [], + 'others': [ + { 'path': 'trait_methods::MyTrait', 'name': 'next' }, + ], + }, +]; diff --git a/tests/rustdoc-js/trait-methods.rs b/tests/rustdoc-js/trait-methods.rs new file mode 100644 index 0000000000000..c88f5edfd55b7 --- /dev/null +++ b/tests/rustdoc-js/trait-methods.rs @@ -0,0 +1,4 @@ +pub trait MyTrait { + type Item; + fn next(&mut self) -> Option; +} From acc74c085d4b3d21c537f5301f088996227586ae Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Fri, 22 Sep 2023 17:48:15 -0700 Subject: [PATCH 2/2] rustdoc: update book with info on type bindings --- .../rustdoc/src/read-documentation/search.md | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/doc/rustdoc/src/read-documentation/search.md b/src/doc/rustdoc/src/read-documentation/search.md index 56a5016d0cee0..1f45bd6c6b827 100644 --- a/src/doc/rustdoc/src/read-documentation/search.md +++ b/src/doc/rustdoc/src/read-documentation/search.md @@ -72,6 +72,7 @@ the standard library and functions that are included in the results list: | [`stdout, [u8]`][stdoutu8] | `Stdout::write` | | [`any -> !`][] | `panic::panic_any` | | [`vec::intoiter -> [T]`][iterasslice] | `IntoIter::as_slice` and `IntoIter::next_chunk` | +| [`iterator, fnmut -> T`][iterreduce] | `Iterator::reduce` and `Iterator::find` | [`usize -> vec`]: ../../std/vec/struct.Vec.html?search=usize%20-%3E%20vec&filter-crate=std [`vec, vec -> bool`]: ../../std/vec/struct.Vec.html?search=vec,%20vec%20-%3E%20bool&filter-crate=std @@ -81,6 +82,7 @@ the standard library and functions that are included in the results list: [`any -> !`]: ../../std/vec/struct.Vec.html?search=any%20-%3E%20!&filter-crate=std [stdoutu8]: ../../std/vec/struct.Vec.html?search=stdout%2C%20[u8]&filter-crate=std [iterasslice]: ../../std/vec/struct.Vec.html?search=vec%3A%3Aintoiter%20->%20[T]&filter-crate=std +[iterreduce]: ../../std/index.html?search=iterator%2C%20fnmut%20->%20T&filter-crate=std ### How type-based search works @@ -95,7 +97,9 @@ After deciding which items are type parameters and which are actual types, it then searches by matching up the function parameters (written before the `->`) and the return types (written after the `->`). Type matching is order-agnostic, and allows items to be left out of the query, but items that are present in the -query must be present in the function for it to match. +query must be present in the function for it to match. The `self` parameter is +treated the same as any other parameter, and `Self` is resolved to the +underlying type's name. Function signature searches can query generics, wrapped in angle brackets, and traits will be normalized like types in the search engine if no type parameters @@ -103,8 +107,37 @@ match them. For example, a function with the signature `fn my_function>(input: I) -> usize` can be matched with the following queries: -* `Iterator -> usize` -* `Iterator -> usize` +* `Iterator -> usize` +* `Iterator -> usize` (you can leave out the `Item=` part) +* `Iterator -> usize` (you can leave out iterator's generic entirely) +* `T -> usize` (you can match with a generic parameter) + +Each of the above queries is progressively looser, except the last one +would not match `dyn Iterator`, since that's not a type parameter. + +If a bound has multiple associated types, specifying the name allows you to +pick which one gets matched. If no name is specified, then the query will +match of any of them. For example, + +```rust +pub trait MyTrait { + type First; + type Second; +} + +/// This function can be found using the following search queries: +/// +/// MyTrait -> bool +/// MyTrait -> bool +/// MyTrait -> bool +/// MyTrait -> bool +/// +/// The following queries, however, will *not* match it: +/// +/// MyTrait -> bool +/// MyTrait -> bool +pub fn my_fn(x: impl MyTrait) -> bool { true } +``` Generics and function parameters are order-agnostic, but sensitive to nesting and number of matches. For example, a function with the signature @@ -134,6 +167,10 @@ Most of these limitations should be addressed in future version of Rustdoc. with that bound, it'll match, but `option -> T where T: Default` cannot be precisely searched for (use `option -> Default`). + * Supertraits, type aliases, and Deref are all ignored. Search mostly + operates on type signatures *as written*, and not as they are + represented within the compiler. + * Type parameters match type parameters, such that `Option` matches `Option`, but never match concrete types in function signatures. A trait named as if it were a type, such as `Option`, will match @@ -183,7 +220,8 @@ slice = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET arg = [type-filter *WS COLON *WS] (path [generics] / slice / [!]) type-sep = COMMA/WS *(COMMA/WS) nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) -generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list ] *(type-sep) +generic-arg-list = *(type-sep) arg [ EQUAL arg ] *(type-sep arg [ EQUAL arg ]) *(type-sep) +generics = OPEN-ANGLE-BRACKET [ generic-arg-list ] *(type-sep) CLOSE-ANGLE-BRACKET return-args = RETURN-ARROW *(type-sep) nonempty-arg-list @@ -230,6 +268,7 @@ DOUBLE-COLON = "::" QUOTE = %x22 COMMA = "," RETURN-ARROW = "->" +EQUAL = "=" ALPHA = %x41-5A / %x61-7A ; A-Z / a-z DIGIT = %x30-39