From 217fe24e52f87eba69162801e52622bfbe61e4c8 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 15 Jun 2023 13:55:45 -0700 Subject: [PATCH 1/9] rustdoc-search: `null`, not `-1`, for missing id This allows us to use negative numbers for others purposes. --- src/librustdoc/html/static/js/externs.js | 2 +- src/librustdoc/html/static/js/search.js | 26 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index f697abd07765a..354f140213a73 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -9,7 +9,7 @@ function initSearch(searchIndex){} /** * @typedef {{ * name: string, - * id: integer, + * id: integer|null, * fullPath: Array, * pathWithoutLast: Array, * pathLast: string, diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 0e270bbcc4028..6ae827dc7782c 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -252,7 +252,7 @@ function initSearch(rawSearchIndex) { /** * Add an item to the type Name->ID map, or, if one already exists, use it. - * Returns the number. If name is "" or null, return -1 (pure generic). + * Returns the number. If name is "" or null, return null (pure generic). * * This is effectively string interning, so that function matching can be * done more quickly. Two types with the same name but different item kinds @@ -264,7 +264,7 @@ function initSearch(rawSearchIndex) { */ function buildTypeMapIndex(name) { if (name === "" || name === null) { - return -1; + return null; } if (typeNameIdMap.has(name)) { @@ -489,7 +489,7 @@ function initSearch(rawSearchIndex) { } return { name: "never", - id: -1, + id: null, fullPath: ["never"], pathWithoutLast: [], pathLast: "never", @@ -531,7 +531,7 @@ function initSearch(rawSearchIndex) { } return { name: name.trim(), - id: -1, + id: null, fullPath: pathSegments, pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), pathLast: pathSegments[pathSegments.length - 1], @@ -660,7 +660,7 @@ function initSearch(rawSearchIndex) { } elems.push({ name: "[]", - id: -1, + id: null, fullPath: ["[]"], pathWithoutLast: [], pathLast: "[]", @@ -1172,7 +1172,7 @@ function initSearch(rawSearchIndex) { const out = []; for (const result of results) { - if (result.id > -1) { + if (result.id !== -1) { const obj = searchIndex[result.id]; obj.dist = result.dist; const res = buildHrefAndPath(obj); @@ -1403,7 +1403,7 @@ function initSearch(rawSearchIndex) { // [unboxing]: // http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); - if (fnType.id === -1 || !( + if (fnType.id === null || !( queryElemSet.has(fnType.id) || (fnType.id === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || (fnType.id === typeNameIdOfArray && queryContainsArrayOrSliceElem) @@ -1564,7 +1564,7 @@ function initSearch(rawSearchIndex) { * @return {boolean} - Returns true if the type matches, false otherwise. */ function checkType(row, elem) { - if (row.id === -1) { + if (row.id === null) { // This is a pure "generic" search, no need to run other checks. return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false; } @@ -1891,7 +1891,7 @@ function initSearch(rawSearchIndex) { if (typeNameIdMap.has(elem.pathLast)) { elem.id = typeNameIdMap.get(elem.pathLast); } else if (!parsedQuery.literalSearch) { - let match = -1; + let match = null; let matchDist = maxEditDistance + 1; let matchName = ""; for (const [name, id] of typeNameIdMap) { @@ -1905,7 +1905,7 @@ function initSearch(rawSearchIndex) { matchName = name; } } - if (match !== -1) { + if (match !== null) { parsedQuery.correction = matchName; } elem.id = match; @@ -2413,7 +2413,7 @@ ${item.displayPath}${name}\ // `0` is used as a sentinel because it's fewer bytes than `null` if (pathIndex === 0) { return { - id: -1, + id: null, ty: null, path: null, generics: generics, @@ -2457,7 +2457,7 @@ ${item.displayPath}${name}\ const pathIndex = functionSearchType[INPUTS_DATA]; if (pathIndex === 0) { inputs = [{ - id: -1, + id: null, ty: null, path: null, generics: [], @@ -2482,7 +2482,7 @@ ${item.displayPath}${name}\ const pathIndex = functionSearchType[OUTPUT_DATA]; if (pathIndex === 0) { output = [{ - id: -1, + id: null, ty: null, path: null, generics: [], From 0b3c617ec0c92081d0eba66914ae11054bb80e52 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Fri, 16 Jun 2023 14:43:28 -0700 Subject: [PATCH 2/9] rustdoc-search: add support for type parameters When writing a type-driven search query in rustdoc, specifically one with more than one query element, non-existent types become generic parameters instead of auto-correcting (which is currently only done for single-element queries) or giving no result. You can also force a generic type parameter by writing `generic:T` (and can force it to not use a generic type parameter with something like `struct:T` or whatever, though if this happens it means the thing you're looking for doesn't exist and will give you no results). There is no syntax provided for specifying type constraints for generic type parameters. When you have a generic type parameter in a search query, it will only match up with generic type parameters in the actual function, not concrete types that match, not concrete types that implement a trait. It also strictly matches based on when they're the same or different, so `option, option -> option` matches `Option::and`, but not `Option::or`. Similarly, `option, option -> option`` matches `Option::or`, but not `Option::and`. --- src/librustdoc/clean/types.rs | 4 - src/librustdoc/html/render/mod.rs | 19 +- src/librustdoc/html/render/search_index.rs | 234 ++++----- src/librustdoc/html/static/js/externs.js | 51 +- src/librustdoc/html/static/js/search.js | 454 +++++++++++------- tests/rustdoc-gui/search-corrections.goml | 42 ++ .../rustdoc-js-std/option-type-signatures.js | 53 ++ tests/rustdoc-js/type-parameters.js | 87 ++++ tests/rustdoc-js/type-parameters.rs | 15 + 9 files changed, 669 insertions(+), 290 deletions(-) create mode 100644 tests/rustdoc-js/type-parameters.js create mode 100644 tests/rustdoc-js/type-parameters.rs diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 9134d5268dab7..98702a9ebc73d 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1637,10 +1637,6 @@ impl Type { matches!(self, Type::Generic(_)) } - pub(crate) fn is_impl_trait(&self) -> bool { - matches!(self, Type::ImplTrait(_)) - } - pub(crate) fn is_unit(&self) -> bool { matches!(self, Type::Tuple(v) if v.is_empty()) } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index aef8f1a74fb5a..26f33d8cf8dc3 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -101,7 +101,7 @@ pub(crate) struct IndexItem { pub(crate) path: String, pub(crate) desc: String, pub(crate) parent: Option, - pub(crate) parent_idx: Option, + pub(crate) parent_idx: Option, pub(crate) search_type: Option, pub(crate) aliases: Box<[Symbol]>, pub(crate) deprecation: Option, @@ -122,7 +122,10 @@ impl Serialize for RenderType { let id = match &self.id { // 0 is a sentinel, everything else is one-indexed None => 0, - Some(RenderTypeId::Index(idx)) => idx + 1, + // concrete type + Some(RenderTypeId::Index(idx)) if *idx >= 0 => idx + 1, + // generic type parameter + Some(RenderTypeId::Index(idx)) => *idx, _ => panic!("must convert render types to indexes before serializing"), }; if let Some(generics) = &self.generics { @@ -140,7 +143,7 @@ impl Serialize for RenderType { pub(crate) enum RenderTypeId { DefId(DefId), Primitive(clean::PrimitiveType), - Index(usize), + Index(isize), } /// Full type of functions/methods in the search index. @@ -148,6 +151,7 @@ pub(crate) enum RenderTypeId { pub(crate) struct IndexItemFunctionType { inputs: Vec, output: Vec, + where_clause: Vec>, } impl Serialize for IndexItemFunctionType { @@ -170,10 +174,17 @@ impl Serialize for IndexItemFunctionType { _ => seq.serialize_element(&self.inputs)?, } match &self.output[..] { - [] => {} + [] if self.where_clause.is_empty() => {} [one] if one.generics.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() { + seq.serialize_element(one)?; + } else { + seq.serialize_element(constraint)?; + } + } seq.end() } } diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 145c7d18dd013..78c443b2257d0 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -68,16 +68,16 @@ pub(crate) fn build_index<'tcx>( // Reduce `DefId` in paths into smaller sequential numbers, // and prune the paths that do not appear in the index. let mut lastpath = ""; - let mut lastpathid = 0usize; + let mut lastpathid = 0isize; // First, on function signatures 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, + map: &mut FxHashMap, itemid: F, - lastpathid: &mut usize, + lastpathid: &mut isize, crate_paths: &mut Vec<(ItemType, Vec)>, item_type: ItemType, path: &[Symbol], @@ -97,9 +97,9 @@ pub(crate) fn build_index<'tcx>( fn convert_render_type( ty: &mut RenderType, cache: &mut Cache, - itemid_to_pathid: &mut FxHashMap, - primitives: &mut FxHashMap, - lastpathid: &mut usize, + itemid_to_pathid: &mut FxHashMap, + primitives: &mut FxHashMap, + lastpathid: &mut isize, crate_paths: &mut Vec<(ItemType, Vec)>, ) { if let Some(generics) = &mut ty.generics { @@ -173,6 +173,18 @@ pub(crate) fn build_index<'tcx>( &mut crate_paths, ); } + for constraint in &mut search_type.where_clause { + for trait_ in &mut constraint[..] { + convert_render_type( + trait_, + cache, + &mut itemid_to_pathid, + &mut primitives, + &mut lastpathid, + &mut crate_paths, + ); + } + } } } @@ -402,7 +414,7 @@ pub(crate) fn get_function_type_for_search<'tcx>( impl_generics: Option<&(clean::Type, clean::Generics)>, cache: &Cache, ) -> Option { - let (mut inputs, mut output) = match *item.kind { + 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), @@ -412,7 +424,7 @@ pub(crate) fn get_function_type_for_search<'tcx>( inputs.retain(|a| a.id.is_some() || a.generics.is_some()); output.retain(|a| a.id.is_some() || a.generics.is_some()); - Some(IndexItemFunctionType { inputs, output }) + Some(IndexItemFunctionType { inputs, output, where_clause }) } fn get_index_type(clean_type: &clean::Type, generics: Vec) -> RenderType { @@ -432,96 +444,48 @@ fn get_index_type_id(clean_type: &clean::Type) -> Option { clean::BorrowedRef { ref type_, .. } | clean::RawPointer(_, ref type_) => { get_index_type_id(type_) } - // The type parameters are converted to generics in `add_generics_and_bounds_as_types` + // 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)), // Not supported yet clean::BareFunction(_) | clean::Generic(_) | clean::ImplTrait(_) - | clean::Tuple(_) | clean::QPath { .. } | clean::Infer => None, } } -/// The point of this function is to replace bounds with types. +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +enum SimplifiedParam { + // other kinds of type parameters are identified by their name + Symbol(Symbol), + // every argument-position impl trait is its own type parameter + Anonymous(isize), +} + +/// The point of this function is to lower generics and types into the simplified form that the +/// frontend search engine can use. /// -/// i.e. `[T, U]` when you have the following bounds: `T: Display, U: Option` will return -/// `[Display, Option]`. If a type parameter has no trait bound, it is discarded. +/// For example, `[T, U, i32]]` where you have the bounds: `T: Display, U: Option` will return +/// `[-1, -2, i32] where -1: Display, -2: Option<-1>`. If a type parameter has no traid bound, it +/// will still get a number. If a constraint is present but not used in the actual types, it will +/// not be added to the map. /// -/// Important note: It goes through generics recursively. So if you have -/// `T: Option>`, it'll go into `Option` and then into `Result`. -#[instrument(level = "trace", skip(tcx, res, cache))] -fn add_generics_and_bounds_as_types<'tcx, 'a>( +/// This function also works recursively. +#[instrument(level = "trace", skip(tcx, res, rgen, cache))] +fn simplify_fn_type<'tcx, 'a>( self_: Option<&'a Type>, generics: &Generics, arg: &'a Type, tcx: TyCtxt<'tcx>, recurse: usize, res: &mut Vec, + rgen: &mut FxHashMap)>, + is_return: bool, cache: &Cache, ) { - fn insert_ty(res: &mut Vec, ty: Type, mut generics: Vec) { - // generics and impl trait are both identified by their generics, - // rather than a type name itself - let anonymous = ty.is_full_generic() || ty.is_impl_trait(); - let generics_empty = generics.is_empty(); - - if anonymous { - if generics_empty { - // This is a type parameter with no trait bounds (for example: `T` in - // `fn f(p: T)`, so not useful for the rustdoc search because we would end up - // with an empty type with an empty name. Let's just discard it. - return; - } else if generics.len() == 1 { - // In this case, no need to go through an intermediate state if the type parameter - // contains only one trait bound. - // - // For example: - // - // `fn foo(r: Option) {}` - // - // In this case, it would contain: - // - // ``` - // [{ - // name: "option", - // generics: [{ - // name: "", - // generics: [ - // name: "Display", - // generics: [] - // }] - // }] - // }] - // ``` - // - // After removing the intermediate (unnecessary) type parameter, it'll become: - // - // ``` - // [{ - // name: "option", - // generics: [{ - // name: "Display", - // generics: [] - // }] - // }] - // ``` - // - // To be noted that it can work if there is ONLY ONE trait bound, otherwise we still - // need to keep it as is! - res.push(generics.pop().unwrap()); - return; - } - } - let index_ty = get_index_type(&ty, generics); - if index_ty.id.is_none() && generics_empty { - return; - } - res.push(index_ty); - } - if recurse >= 10 { // FIXME: remove this whole recurse thing when the recursion bug is fixed // See #59502 for the original issue. @@ -548,88 +512,126 @@ fn add_generics_and_bounds_as_types<'tcx, 'a>( // for its bounds. if let Type::Generic(arg_s) = *arg { // First we check if the bounds are in a `where` predicate... + let mut type_bounds = Vec::new(); for where_pred in generics.where_predicates.iter().filter(|g| match g { WherePredicate::BoundPredicate { ty: Type::Generic(ty_s), .. } => *ty_s == arg_s, _ => false, }) { - let mut ty_generics = Vec::new(); let bounds = where_pred.get_bounds().unwrap_or_else(|| &[]); for bound in bounds.iter() { if let Some(path) = bound.get_trait_path() { let ty = Type::Path { path }; - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, &ty, tcx, recurse + 1, - &mut ty_generics, + &mut type_bounds, + rgen, + is_return, cache, ); } } - insert_ty(res, arg.clone(), ty_generics); } // Otherwise we check if the trait bounds are "inlined" like `T: Option`... if let Some(bound) = generics.params.iter().find(|g| g.is_type() && g.name == arg_s) { - let mut ty_generics = Vec::new(); for bound in bound.get_bounds().unwrap_or(&[]) { if let Some(path) = bound.get_trait_path() { let ty = Type::Path { path }; - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, &ty, tcx, recurse + 1, - &mut ty_generics, + &mut type_bounds, + rgen, + is_return, cache, ); } } - insert_ty(res, arg.clone(), ty_generics); + } + if let Some((idx, _)) = rgen.get(&SimplifiedParam::Symbol(arg_s)) { + res.push(RenderType { id: Some(RenderTypeId::Index(*idx)), generics: 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 }); } } else if let Type::ImplTrait(ref bounds) = *arg { - let mut ty_generics = Vec::new(); + let mut type_bounds = Vec::new(); for bound in bounds { if let Some(path) = bound.get_trait_path() { let ty = Type::Path { path }; - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, &ty, tcx, recurse + 1, - &mut ty_generics, + &mut type_bounds, + rgen, + is_return, cache, ); } } - insert_ty(res, arg.clone(), ty_generics); + 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) }); + } 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 }); + } } else if let Type::Slice(ref ty) = *arg { let mut ty_generics = Vec::new(); - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, &ty, tcx, recurse + 1, &mut ty_generics, + rgen, + is_return, cache, ); - insert_ty(res, arg.clone(), ty_generics); + res.push(get_index_type(arg, ty_generics)); } else if let Type::Array(ref ty, _) = *arg { let mut ty_generics = Vec::new(); - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, &ty, tcx, recurse + 1, &mut ty_generics, + rgen, + is_return, cache, ); - insert_ty(res, arg.clone(), ty_generics); + res.push(get_index_type(arg, ty_generics)); + } else if let Type::Tuple(ref tys) = *arg { + let mut ty_generics = Vec::new(); + for ty in tys { + simplify_fn_type( + self_, + generics, + &ty, + tcx, + recurse + 1, + &mut ty_generics, + rgen, + is_return, + cache, + ); + } + res.push(get_index_type(arg, ty_generics)); } 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. @@ -639,18 +641,26 @@ fn add_generics_and_bounds_as_types<'tcx, 'a>( let mut ty_generics = Vec::new(); if let Some(arg_generics) = arg.generics() { for gen in arg_generics.iter() { - add_generics_and_bounds_as_types( + simplify_fn_type( self_, generics, gen, tcx, recurse + 1, &mut ty_generics, + rgen, + is_return, cache, ); } } - insert_ty(res, arg.clone(), ty_generics); + let id = get_index_type_id(&arg); + if id.is_some() || !ty_generics.is_empty() { + res.push(RenderType { + id, + generics: if ty_generics.is_empty() { None } else { Some(ty_generics) }, + }); + } } } @@ -663,7 +673,7 @@ fn get_fn_inputs_and_outputs<'tcx>( tcx: TyCtxt<'tcx>, impl_generics: Option<&(clean::Type, clean::Generics)>, cache: &Cache, -) -> (Vec, Vec) { +) -> (Vec, Vec, Vec>) { let decl = &func.decl; let combined_generics; @@ -689,21 +699,27 @@ fn get_fn_inputs_and_outputs<'tcx>( (None, &func.generics) }; - let mut all_types = Vec::new(); + let mut rgen: FxHashMap)> = Default::default(); + + let mut arg_types = Vec::new(); for arg in decl.inputs.values.iter() { - let mut args = Vec::new(); - add_generics_and_bounds_as_types(self_, generics, &arg.type_, tcx, 0, &mut args, cache); - if !args.is_empty() { - all_types.extend(args); - } else { - all_types.push(get_index_type(&arg.type_, vec![])); - } + simplify_fn_type( + self_, + generics, + &arg.type_, + tcx, + 0, + &mut arg_types, + &mut rgen, + false, + cache, + ); } let mut ret_types = Vec::new(); - add_generics_and_bounds_as_types(self_, generics, &decl.output, tcx, 0, &mut ret_types, cache); - if ret_types.is_empty() { - ret_types.push(get_index_type(&decl.output, vec![])); - } - (all_types, ret_types) + simplify_fn_type(self_, generics, &decl.output, tcx, 0, &mut ret_types, &mut rgen, true, cache); + + let mut simplified_params = rgen.into_values().collect::>(); + simplified_params.sort_by_key(|(idx, _)| -idx); + (arg_types, ret_types, simplified_params.into_iter().map(|(_idx, traits)| traits).collect()) } diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index 354f140213a73..9595aae4020b0 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -103,7 +103,7 @@ let ResultObject; * * fn something() -> Result * - * If output was allowed to be any RawFunctionType, it would look like this + * If output was allowed to be any RawFunctionType, it would look like thi * * [[], [50, [3, 3]]] * @@ -113,10 +113,56 @@ let ResultObject; * in favor of the pair of types interpretation. This is why the `(number|Array)` * is used instead of `(RawFunctionType|Array)`. * + * The output can be skipped if it's actually unit and there's no type constraints. If thi + * function accepts constrained generics, then the output will be unconditionally emitted, and + * after it will come a list of trait constraints. The position of the item in the list will + * determine which type parameter it is. For example: + * + * [1, 2, 3, 4, 5] + * ^ ^ ^ ^ ^ + * | | | | - generic parameter (-3) of trait 5 + * | | | - generic parameter (-2) of trait 4 + * | | - generic parameter (-1) of trait 3 + * | - this function returns a single value (type 2) + * - this function takes a single input parameter (type 1) + * + * Or, for a less contrived version: + * + * [[[4, -1], 3], [[5, -1]], 11] + * -^^^^^^^---- ^^^^^^^ ^^ + * | | | - generic parameter, roughly `where -1: 11` + * | | | since -1 is the type parameter and 11 the trait + * | | - function output 5<-1> + * | - the overall function signature is something like + * | `fn(4<-1>, 3) -> 5<-1> where -1: 11` + * - function input, corresponds roughly to 4<-1> + * 4 is an index into the `p` array for a type + * -1 is the generic parameter, given by 11 + * + * If a generic parameter has multiple trait constraints, it gets wrapped in an array, just like + * function inputs and outputs: + * + * [-1, -1, [4, 3]] + * ^^^^^^ where -1: 4 + 3 + * + * If a generic parameter's trait constraint has generic parameters, it gets wrapped in the array + * even if only one exists. In other words, the ambiguity of `4<3>` and `4 + 3` is resolved in + * favor of `4 + 3`: + * + * [-1, -1, [[4, 3]]] + * ^^^^^^^^ where -1: 4 + 3 + * + * [-1, -1, [5, [4, 3]]] + * ^^^^^^^^^^^ where -1: 5, -2: 4 + 3 + * + * If a generic parameter has no trait constraints (like in Rust, the `Sized` constraint i + * implied and a fake `?Sized` constraint used to note its absence), it will be filled in with 0. + * * @typedef {( * 0 | * [(number|Array)] | - * [(number|Array), (number|Array)] + * [(number|Array), (number|Array)] | + * Array<(number|Array)> * )} */ let RawFunctionSearchType; @@ -136,6 +182,7 @@ let RawFunctionType; * @typedef {{ * inputs: Array, * output: Array, + * where_clause: Array>, * }} */ let FunctionSearchType; diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 6ae827dc7782c..6301dd728af53 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -33,6 +33,7 @@ const itemTypes = [ "attr", "derive", "traitalias", + "generic", ]; const longItemTypes = [ @@ -67,6 +68,7 @@ const longItemTypes = [ // used for special search precedence const TY_PRIMITIVE = itemTypes.indexOf("primitive"); const TY_KEYWORD = itemTypes.indexOf("keyword"); +const TY_GENERIC = itemTypes.indexOf("generic"); const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; function hasOwnPropertyRustdoc(obj, property) { @@ -974,6 +976,8 @@ function initSearch(rawSearchIndex) { literalSearch: false, error: null, correction: null, + proposeCorrectionFrom: null, + proposeCorrectionTo: null, }; } @@ -1014,64 +1018,10 @@ function initSearch(rawSearchIndex) { /** * Parses the query. * - * The supported syntax by this parser is as follow: + * The supported syntax by this parser is given in the rustdoc book chapter + * /src/doc/rustdoc/src/read-documentation/search.md * - * ident = *(ALPHA / DIGIT / "_") - * path = ident *(DOUBLE-COLON/{WS} ident) [!] - * slice = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET - * arg = [type-filter *WS COLON *WS] (path [generics] / slice) - * type-sep = *WS COMMA *(COMMA) - * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) - * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list ] *(type-sep) - * CLOSE-ANGLE-BRACKET - * return-args = RETURN-ARROW *(type-sep) nonempty-arg-list - * - * exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] - * type-search = [ nonempty-arg-list ] [ return-args ] - * - * query = *WS (exact-search / type-search) *WS - * - * type-filter = ( - * "mod" / - * "externcrate" / - * "import" / - * "struct" / - * "enum" / - * "fn" / - * "type" / - * "static" / - * "trait" / - * "impl" / - * "tymethod" / - * "method" / - * "structfield" / - * "variant" / - * "macro" / - * "primitive" / - * "associatedtype" / - * "constant" / - * "associatedconstant" / - * "union" / - * "foreigntype" / - * "keyword" / - * "existential" / - * "attr" / - * "derive" / - * "traitalias") - * - * OPEN-ANGLE-BRACKET = "<" - * CLOSE-ANGLE-BRACKET = ">" - * OPEN-SQUARE-BRACKET = "[" - * CLOSE-SQUARE-BRACKET = "]" - * COLON = ":" - * DOUBLE-COLON = "::" - * QUOTE = %x22 - * COMMA = "," - * RETURN-ARROW = "->" - * - * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - * DIGIT = %x30-39 - * WS = %x09 / " " + * When adding new things to the parser, add them there, too! * * @param {string} val - The user query * @@ -1348,24 +1298,28 @@ function initSearch(rawSearchIndex) { * This function checks generics in search query `queryElem` can all be found in the * search index (`fnType`), * - * @param {FunctionType} fnType - The object to check. - * @param {QueryElement} queryElem - The element from the parsed query. + * @param {FunctionType} fnType - The object to check. + * @param {QueryElement} queryElem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensIn - Map functions generics to query generics. * * @return {boolean} - Returns true if a match, false otherwise. */ - function checkGenerics(fnType, queryElem) { - return unifyFunctionTypes(fnType.generics, queryElem.generics); + function checkGenerics(fnType, queryElem, whereClause, mgensIn) { + return unifyFunctionTypes(fnType.generics, queryElem.generics, whereClause, mgensIn); } /** * This function checks if a list of search query `queryElems` can all be found in the * search index (`fnTypes`). * - * @param {Array} fnTypes - The objects to check. - * @param {Array} queryElems - The elements from the parsed query. + * @param {Array} fnTypes - The objects to check. + * @param {Array} queryElems - The elements from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensIn - Map function generics to query generics. * * @return {boolean} - Returns true if a match, false otherwise. */ - function unifyFunctionTypes(fnTypes, queryElems) { + function unifyFunctionTypes(fnTypes, queryElems, whereClause, mgensIn) { // This search engine implements order-agnostic unification. There // should be no missing duplicates (generics have "bag semantics"), // and the row is allowed to have extras. @@ -1379,13 +1333,15 @@ function initSearch(rawSearchIndex) { * @type Map */ const queryElemSet = new Map(); + const mgens = new Map(mgensIn); const addQueryElemToQueryElemSet = queryElem => { let currentQueryElemList; - if (queryElemSet.has(queryElem.id)) { - currentQueryElemList = queryElemSet.get(queryElem.id); + const qid = queryElem.id; + if (queryElemSet.has(qid)) { + currentQueryElemList = queryElemSet.get(qid); } else { currentQueryElemList = []; - queryElemSet.set(queryElem.id, currentQueryElemList); + queryElemSet.set(qid, currentQueryElemList); } currentQueryElemList.push(queryElem); }; @@ -1396,44 +1352,98 @@ function initSearch(rawSearchIndex) { * @type Map */ const fnTypeSet = new Map(); - const addFnTypeToFnTypeSet = fnType => { - // Pure generic, or an item that's not matched by any query elems. - // Try [unboxing] it. - // - // [unboxing]: - // http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); - if (fnType.id === null || !( - queryElemSet.has(fnType.id) || - (fnType.id === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || - (fnType.id === typeNameIdOfArray && queryContainsArrayOrSliceElem) - )) { + /** + * If `fnType` is a concrete type with generics, replace it with its generics. + * + * If `fnType` is a generic type parameter, replace it with any traits that it's + * constrained by. This converts `T where T: Trait` into the same representation + * that's directly used by return-position `impl Trait` and by `dyn Trait` in + * all positions, so you can directly write `-> Trait` and get all three. + * + * This seems to correspond to two transforms described in + * http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf + * One of them is actual unboxing (turning `Maybe a` into `a`), while the other is + * equivalent to converting a type parameter into an existential, which Hoogle doesn't + * seem to actually do? I notice that these two searches produce the same result: + * https://hoogle.haskell.org/?hoogle=Eq+-%3E+Set+Eq+-%3E+Set+Eq + * https://hoogle.haskell.org/?hoogle=Ord+-%3E+Set+Ord+-%3E+Set+Ord + * + * Haskell is a bit of a foreign language to me. I just want to be able to look up + * Option combinators without having to actually remember their (mostly arbitrary) + * names, and Haskell claims to already have that problem solved... + * + * @type Map + */ + const unbox = function unbox(fnType) { + if (fnType.id < 0) { + const genid = (-fnType.id) - 1; + if (whereClause && whereClause[genid]) { + for (const trait of whereClause[genid]) { + addFnTypeToFnTypeSet(trait); + } + } + mgens.set(fnType.id, 0); + } else { for (const innerFnType of fnType.generics) { addFnTypeToFnTypeSet(innerFnType); } + } + }; + /** + * @param {FunctionType} fnType + */ + const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) { + const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); + // qsid is an index into the `queryElemSet`, while `fnType.id` is an index into + // the `fnTypeSet`. These are the same, except for generics, where they get mapped. + // If qsid = 0, it means the generic type parameter has undergone instance + // substitution, and + let qsid = fnType.id; + if (fnType.id < 0) { + if (mgens.has(fnType.id)) { + qsid = mgens.get(fnType.id); + } else { + qsid = null; + searching: for (const qid of queryElemSet.keys()) { + if (qid < 0) { + for (const qidMapped of mgens.values()) { + if (qid === qidMapped) { + continue searching; + } + } + mgens.set(fnType.id, qid); + qsid = qid; + break; + } + } + } + } + if (qsid === null || !( + queryElemSet.has(qsid) || + (qsid === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || + (qsid === typeNameIdOfArray && queryContainsArrayOrSliceElem) + )) { + unbox(fnType); return; } - let currentQueryElemList = queryElemSet.get(fnType.id) || []; + let currentQueryElemList = queryElemSet.get(qsid) || []; let matchIdx = currentQueryElemList.findIndex(queryElem => { return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem); + checkGenerics(fnType, queryElem, whereClause, mgens); }); if (matchIdx === -1 && - (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) && + (qsid === typeNameIdOfSlice || qsid === typeNameIdOfArray) && queryContainsArrayOrSliceElem ) { currentQueryElemList = queryElemSet.get(typeNameIdOfArrayOrSlice) || []; matchIdx = currentQueryElemList.findIndex(queryElem => { return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem); + checkGenerics(fnType, queryElem, whereClause, mgens); }); } // None of the query elems match the function type. - // Try [unboxing] it. if (matchIdx === -1) { - for (const innerFnType of fnType.generics) { - addFnTypeToFnTypeSet(innerFnType); - } + unbox(fnType); return; } let currentFnTypeList; @@ -1488,7 +1498,9 @@ function initSearch(rawSearchIndex) { continue; } } - if (queryElem.generics.length === 0 || checkGenerics(fnType, queryElem)) { + if (queryElem.generics.length === 0 + || checkGenerics(fnType, queryElem, whereClause, mgens) + ) { currentFnTypeList.splice(i, 1); const result = doHandleQueryElemList(currentFnTypeList, queryElemList); if (result) { @@ -1499,15 +1511,32 @@ function initSearch(rawSearchIndex) { } return false; }; + /** + * @param {number} id + * @param {[QueryElement]} queryElemList + */ const handleQueryElemList = (id, queryElemList) => { - if (!fnTypeSet.has(id)) { - if (id === typeNameIdOfArrayOrSlice) { + let fsid = id; + if (fsid < 0) { + fsid = null; + if (!mgens) { + return false; + } + for (const [fid, qsid] of mgens) { + if (id === qsid) { + fsid = fid; + break; + } + } + } + if (fsid === null || !fnTypeSet.has(fsid)) { + if (fsid === typeNameIdOfArrayOrSlice) { return handleQueryElemList(typeNameIdOfSlice, queryElemList) || handleQueryElemList(typeNameIdOfArray, queryElemList); } return false; } - const currentFnTypeList = fnTypeSet.get(id); + const currentFnTypeList = fnTypeSet.get(fsid); if (currentFnTypeList.length < queryElemList.length) { // It's not possible for all the query elems to find a match. return false; @@ -1518,14 +1547,14 @@ function initSearch(rawSearchIndex) { // Any items that weren't used for it can be unboxed, and might form // part of the solution for another item. for (const innerFnType of currentFnTypeList) { - addFnTypeToFnTypeSet(innerFnType); + unbox(innerFnType); } - fnTypeSet.delete(id); + fnTypeSet.delete(fsid); } return result; }; - let queryElemSetSize = -1; - while (queryElemSetSize !== queryElemSet.size) { + let queryElemSetSize = Number.MAX_VALUE; + while (queryElemSetSize > queryElemSet.size) { queryElemSetSize = queryElemSet.size; for (const [id, queryElemList] of queryElemSet) { if (handleQueryElemList(id, queryElemList)) { @@ -1533,7 +1562,14 @@ function initSearch(rawSearchIndex) { } } } - return queryElemSetSize === 0; + if (queryElemSetSize === 0) { + for (const [fid, qid] of mgens) { + mgensIn.set(fid, qid); + } + return true; + } else { + return false; + } } /** @@ -1541,13 +1577,14 @@ function initSearch(rawSearchIndex) { * generics (if any). * * @param {Array} list - * @param {QueryElement} elem - The element from the parsed query. + * @param {QueryElement} elem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. * * @return {boolean} - Returns true if found, false otherwise. */ - function checkIfInList(list, elem) { + function checkIfInList(list, elem, whereClause) { for (const entry of list) { - if (checkType(entry, elem)) { + if (checkType(entry, elem, whereClause)) { return true; } } @@ -1559,14 +1596,26 @@ function initSearch(rawSearchIndex) { * generics (if any). * * @param {Row} row - * @param {QueryElement} elem - The element from the parsed query. + * @param {QueryElement} elem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. * * @return {boolean} - Returns true if the type matches, false otherwise. */ - function checkType(row, elem) { + function checkType(row, elem, whereClause) { if (row.id === null) { // This is a pure "generic" search, no need to run other checks. - return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false; + return row.generics.length > 0 + ? checkIfInList(row.generics, elem, whereClause) + : false; + } + + if (row.id < 0 && elem.id >= 0) { + const gid = (-row.id) - 1; + return checkIfInList(whereClause[gid], elem, whereClause); + } + + if (row.id < 0 && elem.id < 0) { + return true; } const matchesExact = row.id === elem.id; @@ -1576,7 +1625,7 @@ function initSearch(rawSearchIndex) { if ((matchesExact || matchesArrayOrSlice) && typePassesFilter(elem.typeFilter, row.ty)) { if (elem.generics.length > 0) { - return checkGenerics(row, elem); + return checkGenerics(row, elem, whereClause, new Map()); } return true; } @@ -1584,7 +1633,7 @@ function initSearch(rawSearchIndex) { // If the current item does not match, try [unboxing] the generic. // [unboxing]: // https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - return checkIfInList(row.generics, elem); + return checkIfInList(row.generics, elem, whereClause); } function checkPath(contains, ty, maxEditDistance) { @@ -1785,13 +1834,15 @@ function initSearch(rawSearchIndex) { const fullId = row.id; const searchWord = searchWords[pos]; - const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem); + const in_args = row.type && row.type.inputs + && checkIfInList(row.type.inputs, elem, row.type.where_clause); if (in_args) { // path_dist is 0 because no parent path information is currently stored // in the search index addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance); } - const returned = row.type && row.type.output && checkIfInList(row.type.output, elem); + const returned = row.type && row.type.output + && checkIfInList(row.type.output, elem, row.type.where_clause); if (returned) { addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance); } @@ -1853,10 +1904,24 @@ function initSearch(rawSearchIndex) { } // If the result is too "bad", we return false and it ends this search. - if (!unifyFunctionTypes(row.type.inputs, parsedQuery.elems)) { + let mgens; + if (row.type.where_clause && row.type.where_clause.length > 0) { + mgens = new Map(); + } + if (!unifyFunctionTypes( + row.type.inputs, + parsedQuery.elems, + row.type.where_clause, + mgens + )) { return; } - if (!unifyFunctionTypes(row.type.output, parsedQuery.returned)) { + if (!unifyFunctionTypes( + row.type.output, + parsedQuery.returned, + row.type.where_clause, + mgens + )) { return; } @@ -1875,6 +1940,11 @@ function initSearch(rawSearchIndex) { } const maxEditDistance = Math.floor(queryLen / 3); + /** + * @type {Map} + */ + const genericSymbols = new Map(); + /** * Convert names to ids in parsed query elements. * This is not used for the "In Names" tab, but is used for the @@ -1910,6 +1980,46 @@ function initSearch(rawSearchIndex) { } elem.id = match; } + if ((elem.id === null && parsedQuery.foundElems > 1 && elem.typeFilter === -1) + || elem.typeFilter === TY_GENERIC) { + if (genericSymbols.has(elem.name)) { + elem.id = genericSymbols.get(elem.name); + } else { + elem.id = -(genericSymbols.size + 1); + genericSymbols.set(elem.name, elem.id); + } + if (elem.typeFilter === -1 && elem.name.length >= 3) { + // Silly heuristic to catch if the user probably meant + // to not write a generic parameter. We don't use it, + // just bring it up. + const maxPartDistance = Math.floor(elem.name.length / 3); + let matchDist = maxPartDistance + 1; + let matchName = ""; + for (const name of typeNameIdMap.keys()) { + const dist = editDistance(name, elem.name, maxPartDistance); + if (dist <= matchDist && dist <= maxPartDistance) { + if (dist === matchDist && matchName > name) { + continue; + } + matchDist = dist; + matchName = name; + } + } + if (matchName !== "") { + parsedQuery.proposeCorrectionFrom = elem.name; + parsedQuery.proposeCorrectionTo = matchName; + } + } + elem.typeFilter = TY_GENERIC; + } + if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) { + // Rust does not have HKT + parsedQuery.error = [ + "Generic type parameter ", + elem.name, + " does not accept generic parameters", + ]; + } for (const elem2 of elem.generics) { convertNameToId(elem2); } @@ -1943,8 +2053,11 @@ function initSearch(rawSearchIndex) { elem = parsedQuery.returned[0]; for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { row = searchIndex[i]; - in_returned = row.type && - unifyFunctionTypes(row.type.output, parsedQuery.returned); + in_returned = row.type && unifyFunctionTypes( + row.type.output, + parsedQuery.returned, + row.type.where_clause, new Map() + ); if (in_returned) { addIntoResults( results_others, @@ -2295,6 +2408,13 @@ ${item.displayPath}${name}\ "Showing results for closest type name " + `"${results.query.correction}" instead.`; } + if (results.query.proposeCorrectionFrom !== null) { + const orig = results.query.proposeCorrectionFrom; + const targ = results.query.proposeCorrectionTo; + output += "

" + + `Type "${orig}" not found and used as generic parameter. ` + + `Consider searching for "${targ}" instead.

`; + } const resultsElem = document.createElement("div"); resultsElem.id = "results"; @@ -2396,37 +2516,54 @@ ${item.displayPath}${name}\ * @return {Array} */ function buildItemSearchTypeAll(types, lowercasePaths) { + return types.map(type => buildItemSearchType(type, lowercasePaths)); + } + + /** + * Converts a single type. + * + * @param {RawFunctionType} type + */ + function buildItemSearchType(type, lowercasePaths) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; - return types.map(type => { - let pathIndex, generics; - if (typeof type === "number") { - pathIndex = type; - generics = []; - } else { - pathIndex = type[PATH_INDEX_DATA]; - generics = buildItemSearchTypeAll( - type[GENERICS_DATA], - lowercasePaths - ); - } + let pathIndex, generics; + if (typeof type === "number") { + pathIndex = type; + generics = []; + } else { + pathIndex = type[PATH_INDEX_DATA]; + generics = buildItemSearchTypeAll( + type[GENERICS_DATA], + lowercasePaths + ); + } + if (pathIndex < 0) { + // types less than 0 are generic parameters + // the actual names of generic parameters aren't stored, since they aren't API + return { + id: pathIndex, + ty: TY_GENERIC, + path: null, + generics, + }; + } + if (pathIndex === 0) { // `0` is used as a sentinel because it's fewer bytes than `null` - if (pathIndex === 0) { - return { - id: null, - ty: null, - path: null, - generics: generics, - }; - } - const item = lowercasePaths[pathIndex - 1]; return { - id: buildTypeMapIndex(item.name), - ty: item.ty, - path: item.path, - generics: generics, + id: null, + ty: null, + path: null, + generics, }; - }); + } + const item = lowercasePaths[pathIndex - 1]; + return { + id: buildTypeMapIndex(item.name), + ty: item.ty, + path: item.path, + generics, + }; } /** @@ -2454,23 +2591,7 @@ ${item.displayPath}${name}\ } let inputs, output; if (typeof functionSearchType[INPUTS_DATA] === "number") { - const pathIndex = functionSearchType[INPUTS_DATA]; - if (pathIndex === 0) { - inputs = [{ - id: null, - ty: null, - path: null, - generics: [], - }]; - } else { - const item = lowercasePaths[pathIndex - 1]; - inputs = [{ - id: buildTypeMapIndex(item.name), - ty: item.ty, - path: item.path, - generics: [], - }]; - } + inputs = [buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths)]; } else { inputs = buildItemSearchTypeAll( functionSearchType[INPUTS_DATA], @@ -2479,23 +2600,7 @@ ${item.displayPath}${name}\ } if (functionSearchType.length > 1) { if (typeof functionSearchType[OUTPUT_DATA] === "number") { - const pathIndex = functionSearchType[OUTPUT_DATA]; - if (pathIndex === 0) { - output = [{ - id: null, - ty: null, - path: null, - generics: [], - }]; - } else { - const item = lowercasePaths[pathIndex - 1]; - output = [{ - id: buildTypeMapIndex(item.name), - ty: item.ty, - path: item.path, - generics: [], - }]; - } + output = [buildItemSearchType(functionSearchType[OUTPUT_DATA], lowercasePaths)]; } else { output = buildItemSearchTypeAll( functionSearchType[OUTPUT_DATA], @@ -2505,8 +2610,15 @@ ${item.displayPath}${name}\ } else { output = []; } + const where_clause = []; + const l = functionSearchType.length; + for (let i = 2; i < l; ++i) { + where_clause.push(typeof functionSearchType[i] === "number" + ? [buildItemSearchType(functionSearchType[i], lowercasePaths)] + : buildItemSearchTypeAll(functionSearchType[i], lowercasePaths)); + } return { - inputs, output, + inputs, output, where_clause, }; } diff --git a/tests/rustdoc-gui/search-corrections.goml b/tests/rustdoc-gui/search-corrections.goml index 5d1b83b35c5ee..46a14f9674975 100644 --- a/tests/rustdoc-gui/search-corrections.goml +++ b/tests/rustdoc-gui/search-corrections.goml @@ -54,3 +54,45 @@ assert-text: ( ".search-corrections", "Type \"notablestructwithlongnamr\" not found. Showing results for closest type name \"notablestructwithlongname\" instead." ) + +// Now, generic correction +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +// Intentionally wrong spelling of "NotableStructWithLongName" +write: (".search-input", "NotableStructWithLongNamr, NotableStructWithLongNamr") +// To be SURE that the search will be run. +press-key: 'Enter' +// Waiting for the search results to appear... +wait-for: "#search-tabs" + +assert-css: (".search-corrections", { + "display": "block" +}) +assert-text: ( + ".search-corrections", + "Type \"notablestructwithlongnamr\" not found and used as generic parameter. Consider searching for \"notablestructwithlongname\" instead." +) + +// Now, generic correction plus error +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +// Intentionally wrong spelling of "NotableStructWithLongName" +write: (".search-input", "NotableStructWithLongNamr,y") +// To be SURE that the search will be run. +press-key: 'Enter' +// Waiting for the search results to appear... +wait-for: "#search-tabs" + +assert-css: (".search-corrections", { + "display": "block" +}) +assert-text: ( + ".search-corrections", + "Type \"notablestructwithlongnamr\" not found and used as generic parameter. Consider searching for \"notablestructwithlongname\" instead." +) + +assert-css: (".error", { + "display": "block" +}) +assert-text: ( + ".error", + "Query parser error: \"Generic type parameter notablestructwithlongnamr does not accept generic parameters\"." +) diff --git a/tests/rustdoc-js-std/option-type-signatures.js b/tests/rustdoc-js-std/option-type-signatures.js index 2599785066145..c5cd6b1d2ef8c 100644 --- a/tests/rustdoc-js-std/option-type-signatures.js +++ b/tests/rustdoc-js-std/option-type-signatures.js @@ -1,3 +1,5 @@ +const FILTER_CRATE = "std"; + const EXPECTED = [ { 'query': 'option, fnonce -> option', @@ -19,4 +21,55 @@ const EXPECTED = [ { 'path': 'std::option::Option', 'name': 'as_mut_slice' }, ], }, + { + 'query': 'option, option -> option', + 'others': [ + { 'path': 'std::option::Option', 'name': 'or' }, + { 'path': 'std::option::Option', 'name': 'xor' }, + ], + }, + { + 'query': 'option, option -> option', + 'others': [ + { 'path': 'std::option::Option', 'name': 'and' }, + { 'path': 'std::option::Option', 'name': 'zip' }, + ], + }, + { + 'query': 'option, option -> option', + 'others': [ + { 'path': 'std::option::Option', 'name': 'zip' }, + ], + }, + { + 'query': 'option, option -> option', + 'others': [ + { 'path': 'std::option::Option', 'name': 'zip' }, + ], + }, + { + 'query': 'option, e -> result', + 'others': [ + { 'path': 'std::option::Option', 'name': 'ok_or' }, + { 'path': 'std::result::Result', 'name': 'transpose' }, + ], + }, + { + 'query': 'result, e> -> option>', + 'others': [ + { 'path': 'std::result::Result', 'name': 'transpose' }, + ], + }, + { + 'query': 'option, option -> bool', + 'others': [ + { 'path': 'std::option::Option', 'name': 'eq' }, + ], + }, + { + 'query': 'option> -> option', + 'others': [ + { 'path': 'std::option::Option', 'name': 'flatten' }, + ], + }, ]; diff --git a/tests/rustdoc-js/type-parameters.js b/tests/rustdoc-js/type-parameters.js new file mode 100644 index 0000000000000..e695f189bb672 --- /dev/null +++ b/tests/rustdoc-js/type-parameters.js @@ -0,0 +1,87 @@ +// exact-check +// ignore-order + +const EXPECTED = [ + { + query: '-> trait:Some', + others: [ + { path: 'foo', name: 'alef' }, + { path: 'foo', name: 'alpha' }, + ], + }, + { + query: '-> generic:T', + others: [ + { path: 'foo', name: 'bet' }, + { path: 'foo', name: 'alef' }, + { path: 'foo', name: 'beta' }, + ], + }, + { + query: 'A -> B', + others: [ + { path: 'foo', name: 'bet' }, + ], + }, + { + query: 'A -> A', + others: [ + { path: 'foo', name: 'beta' }, + ], + }, + { + query: 'A, A', + others: [ + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'A, B', + others: [ + { path: 'foo', name: 'other' }, + ], + }, + { + query: 'Other, Other', + others: [ + { path: 'foo', name: 'other' }, + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'generic:T', + in_args: [ + { path: 'foo', name: 'bet' }, + { path: 'foo', name: 'beta' }, + { path: 'foo', name: 'other' }, + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'generic:Other', + in_args: [ + { path: 'foo', name: 'bet' }, + { path: 'foo', name: 'beta' }, + { path: 'foo', name: 'other' }, + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'trait:Other', + in_args: [ + { path: 'foo', name: 'other' }, + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'Other', + in_args: [ + { path: 'foo', name: 'other' }, + { path: 'foo', name: 'alternate' }, + ], + }, + { + query: 'trait:T', + in_args: [], + }, +]; diff --git a/tests/rustdoc-js/type-parameters.rs b/tests/rustdoc-js/type-parameters.rs new file mode 100644 index 0000000000000..cda5e26171fca --- /dev/null +++ b/tests/rustdoc-js/type-parameters.rs @@ -0,0 +1,15 @@ +#![crate_name="foo"] + +pub trait Some {} +impl Some for () {} +pub trait Other {} +impl Other for () {} + +pub fn alef() -> T { loop {} } +pub fn alpha() -> impl Some { } + +pub fn bet(t: T) -> U { loop {} } +pub fn beta(t: T) -> T {} + +pub fn other(t: T, u: U) { loop {} } +pub fn alternate(t: T, u: T) { loop {} } From b6bb06ca5d3a5edac2c72549101fd8f932f7bc04 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Fri, 16 Jun 2023 17:35:02 -0700 Subject: [PATCH 3/9] rustdoc: write detailed chapter on search engine --- src/doc/rustdoc/src/SUMMARY.md | 1 + src/doc/rustdoc/src/how-to-read-rustdoc.md | 55 +--- .../rustdoc/src/read-documentation/search.md | 237 ++++++++++++++++++ 3 files changed, 243 insertions(+), 50 deletions(-) create mode 100644 src/doc/rustdoc/src/read-documentation/search.md diff --git a/src/doc/rustdoc/src/SUMMARY.md b/src/doc/rustdoc/src/SUMMARY.md index 12a8b2b8db4b6..3b6883c0d557d 100644 --- a/src/doc/rustdoc/src/SUMMARY.md +++ b/src/doc/rustdoc/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Command-line arguments](command-line-arguments.md) - [How to read rustdoc output](how-to-read-rustdoc.md) - [In-doc settings](read-documentation/in-doc-settings.md) + - [Search](read-documentation/search.md) - [How to write documentation](how-to-write-documentation.md) - [What to include (and exclude)](write-documentation/what-to-include.md) - [The `#[doc]` attribute](write-documentation/the-doc-attribute.md) diff --git a/src/doc/rustdoc/src/how-to-read-rustdoc.md b/src/doc/rustdoc/src/how-to-read-rustdoc.md index 9deb7009cfe00..1f594e6b80ea1 100644 --- a/src/doc/rustdoc/src/how-to-read-rustdoc.md +++ b/src/doc/rustdoc/src/how-to-read-rustdoc.md @@ -59,56 +59,11 @@ or the current item whose documentation is being displayed. ## The Theme Picker and Search Interface When viewing `rustdoc`'s output in a browser with JavaScript enabled, -a dynamic interface appears at the top of the page composed of the search -interface, help screen, and options. - -### The Search Interface - -Typing in the search bar instantly searches the available documentation for -the string entered with a fuzzy matching algorithm that is tolerant of minor -typos. - -By default, the search results given are "In Names", -meaning that the fuzzy match is made against the names of items. -Matching names are shown on the left, and the first few words of their -descriptions are given on the right. -By clicking an item, you will navigate to its particular documentation. - -There are two other sets of results, shown as tabs in the search results pane. -"In Parameters" shows matches for the string in the types of parameters to -functions, and "In Return Types" shows matches in the return types of functions. -Both are very useful when looking for a function whose name you can't quite -bring to mind when you know the type you have or want. - -Names in the search interface can be prefixed with an item type followed by a -colon (such as `mod:`) to restrict the results to just that kind of item. Also, -searching for `println!` will search for a macro named `println`, just like -searching for `macro:println` does. - -Function signature searches can query generics, wrapped in angle brackets, and -traits are normalized like types in the search engine. For example, a function -with the signature `fn my_function>(input: I) -> usize` -can be matched with the following queries: - -* `Iterator -> usize` -* `trait:Iterator -> primitive:usize` -* `Iterator -> usize` - -Generics and function parameters are order-agnostic, but sensitive to nesting -and number of matches. For example, a function with the signature -`fn read_all(&mut self: impl Read) -> Result, Error>` -will match these queries: - -* `Read -> Result, Error>` -* `Read -> Result` -* `Read -> Result>` - -But it *does not* match `Result` or `Result>`. - -Function signature searches also support arrays and slices. The explicit name -`primitive:slice` and `primitive:array` can be used to match a slice -or array of bytes, while square brackets `[u8]` will match either one. Empty -square brackets, `[]`, will match any slice regardless of what it contains. +a dynamic interface appears at the top of the page composed of the [search] +interface, help screen, and [options]. + +[options]: read-documentation/in-doc-settings.html +[search]: read-documentation/search.md Paths are supported as well, you can look for `Vec::new` or `Option::Some` or even `module::module_child::another_child::struct::field`. Whitespace characters diff --git a/src/doc/rustdoc/src/read-documentation/search.md b/src/doc/rustdoc/src/read-documentation/search.md new file mode 100644 index 0000000000000..5f5c52dd09e03 --- /dev/null +++ b/src/doc/rustdoc/src/read-documentation/search.md @@ -0,0 +1,237 @@ +# Rustdoc search + +Typing in the search bar instantly searches the available documentation, +matching either the name and path of an item, or a function's approximate +type signature. + +## Search By Name + +To search by the name of an item (items include modules, types, traits, +functions, and macros), write its name or path. As a special case, the parts +of a path that normally get divided by `::` double colons can instead be +separated by spaces. For example: + + * [`vec new`] and [`vec::new`] both show the function `std::vec::Vec::new` + as a result. + * [`vec`], [`vec vec`], [`std::vec`], and [`std::vec::Vec`] all include the struct + `std::vec::Vec` itself in the results (and all but the last one also + include the module in the results). + +[`vec new`]: ../../std/vec/struct.Vec.html?search=vec%20new&filter-crate=std +[`vec::new`]: ../../std/vec/struct.Vec.html?search=vec::new&filter-crate=std +[`vec`]: ../../std/vec/struct.Vec.html?search=vec&filter-crate=std +[`vec vec`]: ../../std/vec/struct.Vec.html?search=vec%20vec&filter-crate=std +[`std::vec`]: ../../std/vec/struct.Vec.html?search=std::vec&filter-crate=std +[`std::vec::Vec`]: ../../std/vec/struct.Vec.html?search=std::vec::Vec&filter-crate=std +[`std::vec::Vec`]: ../../std/vec/struct.Vec.html?search=std::vec::Vec&filter-crate=std + +As a quick way to trim down the list of results, there's a drop-down selector +below the search input, labeled "Results in \[std\]". Clicking it can change +which crate is being searched. + +Rustdoc uses a fuzzy matching function that can tolerate typos for this, +though it's based on the length of the name that's typed in, so a good example +of how this works would be [`HahsMap`]. To avoid this, wrap the item in quotes, +searching for `"HahsMap"` (in this example, no results will be returned). + +[`HahsMap`]: ../../std/collections/struct.HashMap.html?search=HahsMap&filter-crate=std + +### Tabs in the Search By Name interface + +In fact, using [`HahsMap`] again as the example, it tells you that you're +using "In Names" by default, but also lists two other tabs below the crate +drop-down: "In Parameters" and "In Return Types". + +These two tabs are lists of functions, defined on the closest matching type +to the search (for `HahsMap`, it loudly auto-corrects to `hashmap`). This +auto-correct only kicks in if nothing is found that matches the literal. + +These tabs are not just methods. For example, searching the alloc crate for +[`Layout`] also lists functions that accept layouts even though they're +methods on the allocator or free functions. + +[`Layout`]: ../../alloc/index.html?search=Layout&filter-crate=alloc + +## Searching By Type Signature for functions + +If you know more specifically what the function you want to look at does, +Rustdoc can search by more than one type at once in the parameters and return +value. Multiple parameters are separated by `,` commas, and the return value +is written with after a `->` arrow. + +Before describing the syntax in more detail, here's a few sample searches of +the standard library and functions that are included in the results list: + +| Query | Results | +|-------|--------| +| [`usize -> vec`][] | `slice::repeat` and `Vec::with_capacity` | +| [`vec, vec -> bool`][] | `Vec::eq` | +| [`option, fnonce -> option`][] | `Option::map` and `Option::and_then` | +| [`option, fnonce -> option`][] | `Option::filter` and `Option::inspect` | +| [`option -> default`][] | `Option::unwrap_or_default` | +| [`stdout, [u8]`][stdoutu8] | `Stdout::write` | +| [`any -> !`][] | `panic::panic_any` | + +[`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 +[`option, fnonce -> option`]: ../../std/vec/struct.Vec.html?search=option%2C%20fnonce%20->%20option&filter-crate=std +[`option, fnonce -> option`]: ../../std/vec/struct.Vec.html?search=option%2C%20fnonce%20->%20option&filter-crate=std +[`option -> default`]: ../../std/vec/struct.Vec.html?search=option%20-%3E%20default&filter-crate=std +[`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 + +### How type-based search works + +In a complex type-based search, Rustdoc always treats every item as literal. +If a name is used and nothing in the docs matches the individual item, such as +a typo-ed [`uize -> vec`][] search, the item `uize` is treated as a generic +type parameter (resulting in `vec::from` and other generic vec constructors). + +[`uize -> vec`]: ../../std/vec/struct.Vec.html?search=uize%20-%3E%20vec&filter-crate=std + +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. + +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 +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` + +Generics and function parameters are order-agnostic, but sensitive to nesting +and number of matches. For example, a function with the signature +`fn read_all(&mut self: impl Read) -> Result, Error>` +will match these queries: + +* `Read -> Result, Error>` +* `Read -> Result` +* `Read -> Result>` + +But it *does not* match `Result` or `Result>`. + +Function signature searches also support arrays and slices. The explicit name +`primitive:slice` and `primitive:array` can be used to match a slice +or array of bytes, while square brackets `[u8]` will match either one. Empty +square brackets, `[]`, will match any slice or array regardless of what +it contains, while a slice with a type parameter, like `[T]`, will only match +functions that actually operate on generic slices. + +### Limitations and quirks of type-based search + +Type-based search is still a buggy, experimental, work-in-progress feature. +Most of these limitations should be addressed in future version of Rustdoc. + + * There's no way to write trait constraints on generic parameters. + You can name traits directly, and if there's a type parameter + with that bound, it'll match, but `option -> T where T: Default` + cannot be precisely searched for (use `option -> Default`). + + * 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 + a type parameter constrained by that trait, such as + `Option where T: Read`, as well as matching `dyn Trait` and + `impl Trait`. + + * `impl Trait` in argument position is treated exactly like a type + parameter, but in return position it will not match type parameters. + + * Any type named in a complex type-based search will be assumed to be a + type parameter if nothing matching the name exactly is found. If you + want to force a type parameter, write `generic:T` and it will be used + as a type parameter even if a matching name is found. If you know + that you don't want a type parameter, you can force it to match + something else by giving it a different prefix like `struct:T`. + + * It's impossible to search for references, pointers, or tuples. The + wrapped types can be searched for, so a function that takes `&File` can + be found with `File`, but you'll get a parse error when typing an `&` + into the search field. Similarly, `Option<(T, U)>` can be matched with + `Option`, but `(` will give a parse error. + + * Path searches, like `hash_map::Entry`, don't work in type-based search. + + * Searching for lifetimes is not supported. + + * It's impossible to search for closures based on their parameters or + return values. + + * It's impossible to search based on the length of an array. + +## Item filtering + +Names in the search interface can be prefixed with an item type followed by a +colon (such as `mod:`) to restrict the results to just that kind of item. Also, +searching for `println!` will search for a macro named `println`, just like +searching for `macro:println` does. The complete list of available filters is +given under the ? Help area, and in the detailed syntax below. + +Item filters can be used in both name-based and type signature-based searches. + +## Search query syntax + +```text +ident = *(ALPHA / DIGIT / "_") +path = ident *(DOUBLE-COLON ident) [!] +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) + CLOSE-ANGLE-BRACKET +return-args = RETURN-ARROW *(type-sep) nonempty-arg-list + +exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] +type-search = [ nonempty-arg-list ] [ return-args ] + +query = *WS (exact-search / type-search) *WS + +type-filter = ( + "mod" / + "externcrate" / + "import" / + "struct" / + "enum" / + "fn" / + "type" / + "static" / + "trait" / + "impl" / + "tymethod" / + "method" / + "structfield" / + "variant" / + "macro" / + "primitive" / + "associatedtype" / + "constant" / + "associatedconstant" / + "union" / + "foreigntype" / + "keyword" / + "existential" / + "attr" / + "derive" / + "traitalias" / + "generic") + +OPEN-ANGLE-BRACKET = "<" +CLOSE-ANGLE-BRACKET = ">" +OPEN-SQUARE-BRACKET = "[" +CLOSE-SQUARE-BRACKET = "]" +COLON = ":" +DOUBLE-COLON = "::" +QUOTE = %x22 +COMMA = "," +RETURN-ARROW = "->" + +ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +DIGIT = %x30-39 +WS = %x09 / " " +``` From 89a4c7f552558c293b29437b4b79223786f1923d Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Aug 2023 11:22:21 -0700 Subject: [PATCH 4/9] rustdoc: bug fix for `-> option` --- src/librustdoc/html/static/js/externs.js | 1 + src/librustdoc/html/static/js/search.js | 5 ++++- tests/rustdoc-js-std/option-type-signatures.js | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index 9595aae4020b0..c7811b43d1641 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -37,6 +37,7 @@ let ParserState; * args: Array, * returned: Array, * foundElems: number, + * totalElems: number, * literalSearch: boolean, * corrections: Array<{from: string, to: integer}>, * }} diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 6301dd728af53..fb478cbff3c29 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -973,6 +973,8 @@ function initSearch(rawSearchIndex) { returned: [], // Total number of "top" elements (does not include generics). foundElems: 0, + // Total number of elements (includes generics). + totalElems: 0, literalSearch: false, error: null, correction: null, @@ -1074,6 +1076,7 @@ function initSearch(rawSearchIndex) { query.literalSearch = parserState.totalElems > 1; } query.foundElems = query.elems.length + query.returned.length; + query.totalElems = parserState.totalElems; return query; } @@ -1980,7 +1983,7 @@ function initSearch(rawSearchIndex) { } elem.id = match; } - if ((elem.id === null && parsedQuery.foundElems > 1 && elem.typeFilter === -1) + if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1) || elem.typeFilter === TY_GENERIC) { if (genericSymbols.has(elem.name)) { elem.id = genericSymbols.get(elem.name); diff --git a/tests/rustdoc-js-std/option-type-signatures.js b/tests/rustdoc-js-std/option-type-signatures.js index c5cd6b1d2ef8c..37bb3b14ab381 100644 --- a/tests/rustdoc-js-std/option-type-signatures.js +++ b/tests/rustdoc-js-std/option-type-signatures.js @@ -72,4 +72,10 @@ const EXPECTED = [ { 'path': 'std::option::Option', 'name': 'flatten' }, ], }, + { + 'query': 'option', + 'returned': [ + { 'path': 'std::result::Result', 'name': 'ok' }, + ], + }, ]; From 60688500089f8e182e35f7ae540e9941734053a4 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Aug 2023 12:27:58 -0700 Subject: [PATCH 5/9] rustdoc: fix test case for generics that look like names --- src/librustdoc/html/static/js/search.js | 3 ++- src/tools/rustdoc-js/tester.js | 4 +++- tests/rustdoc-js/generics-trait.js | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index fb478cbff3c29..b867311aca103 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -1983,7 +1983,8 @@ function initSearch(rawSearchIndex) { } elem.id = match; } - if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1) + if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 + && elem.generics.length === 0) || elem.typeFilter === TY_GENERIC) { if (genericSymbols.has(elem.name)) { elem.id = genericSymbols.get(elem.name); diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index 416517d15f5db..c7e6dd3615e94 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -23,7 +23,9 @@ function contentToDiffLine(key, value) { } function shouldIgnoreField(fieldName) { - return fieldName === "query" || fieldName === "correction"; + return fieldName === "query" || fieldName === "correction" || + fieldName === "proposeCorrectionFrom" || + fieldName === "proposeCorrectionTo"; } // This function is only called when no matching result was found and therefore will only display diff --git a/tests/rustdoc-js/generics-trait.js b/tests/rustdoc-js/generics-trait.js index 4ccfb8f4e4d02..a71393b5e0502 100644 --- a/tests/rustdoc-js/generics-trait.js +++ b/tests/rustdoc-js/generics-trait.js @@ -12,11 +12,15 @@ const EXPECTED = [ ], }, { - 'query': 'Result', - 'correction': null, + 'query': 'Resulx', 'in_args': [], 'returned': [], }, + { + 'query': 'Result', + 'proposeCorrectionFrom': 'SomeTraiz', + 'proposeCorrectionTo': 'SomeTrait', + }, { 'query': 'OtherThingxxxxxxxx', 'correction': null, From f42f357a73fb43c4225ca9c6f24da1f9cec5c9ae Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Aug 2023 14:06:24 -0700 Subject: [PATCH 6/9] rustdoc: update tests for generic parsing and correction --- tests/rustdoc-gui/search-corrections.goml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/rustdoc-gui/search-corrections.goml b/tests/rustdoc-gui/search-corrections.goml index 46a14f9674975..aeb3c9b31a3fd 100644 --- a/tests/rustdoc-gui/search-corrections.goml +++ b/tests/rustdoc-gui/search-corrections.goml @@ -75,7 +75,7 @@ assert-text: ( // Now, generic correction plus error go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" // Intentionally wrong spelling of "NotableStructWithLongName" -write: (".search-input", "NotableStructWithLongNamr,y") +write: (".search-input", "Foo,y") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... @@ -89,6 +89,14 @@ assert-text: ( "Type \"notablestructwithlongnamr\" not found and used as generic parameter. Consider searching for \"notablestructwithlongname\" instead." ) +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +// Intentionally wrong spelling of "NotableStructWithLongName" +write: (".search-input", "generic:NotableStructWithLongNamr,y") +// To be SURE that the search will be run. +press-key: 'Enter' +// Waiting for the search results to appear... +wait-for: "#search-tabs" + assert-css: (".error", { "display": "block" }) From 9ccb217422628107b9b6388a30879ec0866ebc9c Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sun, 3 Sep 2023 14:42:54 -0700 Subject: [PATCH 7/9] Update docs since path-based type search works now --- src/doc/rustdoc/src/read-documentation/search.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/doc/rustdoc/src/read-documentation/search.md b/src/doc/rustdoc/src/read-documentation/search.md index 5f5c52dd09e03..d794dbbf250c1 100644 --- a/src/doc/rustdoc/src/read-documentation/search.md +++ b/src/doc/rustdoc/src/read-documentation/search.md @@ -71,6 +71,7 @@ the standard library and functions that are included in the results list: | [`option -> default`][] | `Option::unwrap_or_default` | | [`stdout, [u8]`][stdoutu8] | `Stdout::write` | | [`any -> !`][] | `panic::panic_any` | +| [`vec::intoiter -> [T]`][iterasslice] | `IntoIter::as_slice` | [`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 @@ -79,10 +80,11 @@ the standard library and functions that are included in the results list: [`option -> default`]: ../../std/vec/struct.Vec.html?search=option%20-%3E%20default&filter-crate=std [`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 ### How type-based search works -In a complex type-based search, Rustdoc always treats every item as literal. +In a complex type-based search, Rustdoc always treats every item's name as literal. If a name is used and nothing in the docs matches the individual item, such as a typo-ed [`uize -> vec`][] search, the item `uize` is treated as a generic type parameter (resulting in `vec::from` and other generic vec constructors). @@ -155,8 +157,6 @@ Most of these limitations should be addressed in future version of Rustdoc. into the search field. Similarly, `Option<(T, U)>` can be matched with `Option`, but `(` will give a parse error. - * Path searches, like `hash_map::Entry`, don't work in type-based search. - * Searching for lifetimes is not supported. * It's impossible to search for closures based on their parameters or From 269cb579479ab950e85a2e4078810501c29d7465 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 9 Sep 2023 16:49:29 -0700 Subject: [PATCH 8/9] rustdoc-search: fix bugs when unboxing and reordering combine --- src/librustdoc/html/static/js/search.js | 510 ++++++++++-------- .../rustdoc-js-std/option-type-signatures.js | 5 +- tests/rustdoc-js-std/vec-type-signatures.js | 22 + tests/rustdoc-js/generics-match-ambiguity.js | 1 + tests/rustdoc-js/generics-unbox.js | 38 ++ tests/rustdoc-js/generics-unbox.rs | 36 ++ 6 files changed, 376 insertions(+), 236 deletions(-) create mode 100644 tests/rustdoc-js-std/vec-type-signatures.js create mode 100644 tests/rustdoc-js/generics-unbox.js create mode 100644 tests/rustdoc-js/generics-unbox.rs diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index b867311aca103..407bf5f2c5fe1 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -3,6 +3,17 @@ "use strict"; +// polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced +if (!Array.prototype.toSpliced) { + // Can't use arrow functions, because we want `this` + Array.prototype.toSpliced = function() { + const me = this.slice(); + Array.prototype.splice.apply(me, arguments); + return me; + }; +} + (function() { // This mapping table should match the discriminants of // `rustdoc::formats::item_type::ItemType` type in Rust. @@ -1301,278 +1312,311 @@ function initSearch(rawSearchIndex) { * This function checks generics in search query `queryElem` can all be found in the * search index (`fnType`), * - * @param {FunctionType} fnType - The object to check. - * @param {QueryElement} queryElem - The element from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensIn - Map functions generics to query generics. + * This function returns `true` if it matches, and also writes the results to mgensInout. + * It returns `false` if no match is found, and leaves mgensInout untouched. + * + * @param {FunctionType} fnType - The object to check. + * @param {QueryElement} queryElem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensInout - Map functions generics to query generics. * * @return {boolean} - Returns true if a match, false otherwise. */ - function checkGenerics(fnType, queryElem, whereClause, mgensIn) { - return unifyFunctionTypes(fnType.generics, queryElem.generics, whereClause, mgensIn); + function checkGenerics(fnType, queryElem, whereClause, mgensInout) { + return unifyFunctionTypes( + fnType.generics, + queryElem.generics, + whereClause, + mgensInout, + mgens => { + if (mgensInout) { + for (const [fid, qid] of mgens.entries()) { + mgensInout.set(fid, qid); + } + } + return true; + } + ); } /** * This function checks if a list of search query `queryElems` can all be found in the * search index (`fnTypes`). * - * @param {Array} fnTypes - The objects to check. - * @param {Array} queryElems - The elements from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensIn - Map function generics to query generics. + * This function returns `true` on a match, or `false` if none. If `solutionCb` is + * supplied, it will call that function with mgens, and that callback can accept or + * reject the result bu returning `true` or `false`. If the callback returns false, + * then this function will try with a different solution, or bail with false if it + * runs out of candidates. + * + * @param {Array} fnTypes - The objects to check. + * @param {Array} queryElems - The elements from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensIn + * - Map functions generics to query generics (never modified). + * @param {null|Map -> bool} solutionCb - Called for each `mgens` solution. * * @return {boolean} - Returns true if a match, false otherwise. */ - function unifyFunctionTypes(fnTypes, queryElems, whereClause, mgensIn) { - // This search engine implements order-agnostic unification. There - // should be no missing duplicates (generics have "bag semantics"), - // and the row is allowed to have extras. + function unifyFunctionTypes(fnTypesIn, queryElems, whereClause, mgensIn, solutionCb) { + /** + * @type Map + */ + let mgens = new Map(mgensIn); if (queryElems.length === 0) { - return true; + return !solutionCb || solutionCb(mgens); } - if (!fnTypes || fnTypes.length === 0) { + if (!fnTypesIn || fnTypesIn.length === 0) { return false; } + const ql = queryElems.length; + let fl = fnTypesIn.length; /** - * @type Map + * @type Array */ - const queryElemSet = new Map(); - const mgens = new Map(mgensIn); - const addQueryElemToQueryElemSet = queryElem => { - let currentQueryElemList; - const qid = queryElem.id; - if (queryElemSet.has(qid)) { - currentQueryElemList = queryElemSet.get(qid); - } else { - currentQueryElemList = []; - queryElemSet.set(qid, currentQueryElemList); - } - currentQueryElemList.push(queryElem); - }; - for (const queryElem of queryElems) { - addQueryElemToQueryElemSet(queryElem); - } + let fnTypes = fnTypesIn.slice(); /** - * @type Map - */ - const fnTypeSet = new Map(); - /** - * If `fnType` is a concrete type with generics, replace it with its generics. - * - * If `fnType` is a generic type parameter, replace it with any traits that it's - * constrained by. This converts `T where T: Trait` into the same representation - * that's directly used by return-position `impl Trait` and by `dyn Trait` in - * all positions, so you can directly write `-> Trait` and get all three. + * loop works by building up a solution set in the working arrays + * fnTypes gets mutated in place to make this work, while queryElems + * is left alone * - * This seems to correspond to two transforms described in - * http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - * One of them is actual unboxing (turning `Maybe a` into `a`), while the other is - * equivalent to converting a type parameter into an existential, which Hoogle doesn't - * seem to actually do? I notice that these two searches produce the same result: - * https://hoogle.haskell.org/?hoogle=Eq+-%3E+Set+Eq+-%3E+Set+Eq - * https://hoogle.haskell.org/?hoogle=Ord+-%3E+Set+Ord+-%3E+Set+Ord + * vvvvvvv `i` points here + * queryElems = [ good, good, good, unknown, unknown ], + * fnTypes = [ good, good, good, unknown, unknown ], + * ---------------- ^^^^^^^^^^^^^^^^ `j` iterates after `i`, + * | looking for candidates + * everything before `i` is the + * current working solution * - * Haskell is a bit of a foreign language to me. I just want to be able to look up - * Option combinators without having to actually remember their (mostly arbitrary) - * names, and Haskell claims to already have that problem solved... + * Everything in the current working solution is known to be a good + * match, but it might not be the match we wind up going with, because + * there might be more than one candidate match, and we need to try them all + * before giving up. So, to handle this, it backtracks on failure. * - * @type Map + * @type Array<{ + * "fnTypesScratch": Array, + * "queryElemsOffset": integer, + * "fnTypesOffset": integer + * }> */ - const unbox = function unbox(fnType) { - if (fnType.id < 0) { - const genid = (-fnType.id) - 1; - if (whereClause && whereClause[genid]) { - for (const trait of whereClause[genid]) { - addFnTypeToFnTypeSet(trait); + const backtracking = []; + let i = 0; + let j = 0; + const backtrack = () => { + while (backtracking.length !== 0) { + // this session failed, but there are other possible solutions + // to backtrack, reset to (a copy of) the old array, do the swap or unboxing + const { + fnTypesScratch, + mgensScratch, + queryElemsOffset, + fnTypesOffset, + unbox, + } = backtracking.pop(); + mgens = new Map(mgensScratch); + const fnType = fnTypesScratch[fnTypesOffset]; + const queryElem = queryElems[queryElemsOffset]; + if (unbox) { + if (fnType.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { + continue; + } + mgens.set(fnType.id, 0); } - } - mgens.set(fnType.id, 0); - } else { - for (const innerFnType of fnType.generics) { - addFnTypeToFnTypeSet(innerFnType); - } - } - }; - /** - * @param {FunctionType} fnType - */ - const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) { - const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); - // qsid is an index into the `queryElemSet`, while `fnType.id` is an index into - // the `fnTypeSet`. These are the same, except for generics, where they get mapped. - // If qsid = 0, it means the generic type parameter has undergone instance - // substitution, and - let qsid = fnType.id; - if (fnType.id < 0) { - if (mgens.has(fnType.id)) { - qsid = mgens.get(fnType.id); + const generics = fnType.id < 0 ? + whereClause[(-fnType.id) - 1] : + fnType.generics; + fnTypes = fnTypesScratch.toSpliced(fnTypesOffset, 1, ...generics); + fl = fnTypes.length; + // re-run the matching algorithm on this item + i = queryElemsOffset - 1; } else { - qsid = null; - searching: for (const qid of queryElemSet.keys()) { - if (qid < 0) { - for (const qidMapped of mgens.values()) { - if (qid === qidMapped) { - continue searching; - } - } - mgens.set(fnType.id, qid); - qsid = qid; - break; + if (fnType.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) { + continue; } + mgens.set(fnType.id, queryElem.id); } + fnTypes = fnTypesScratch.slice(); + fl = fnTypes.length; + const tmp = fnTypes[queryElemsOffset]; + fnTypes[queryElemsOffset] = fnTypes[fnTypesOffset]; + fnTypes[fnTypesOffset] = tmp; + // this is known as a good match; go to the next one + i = queryElemsOffset; } - } - if (qsid === null || !( - queryElemSet.has(qsid) || - (qsid === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || - (qsid === typeNameIdOfArray && queryContainsArrayOrSliceElem) - )) { - unbox(fnType); - return; - } - let currentQueryElemList = queryElemSet.get(qsid) || []; - let matchIdx = currentQueryElemList.findIndex(queryElem => { - return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem, whereClause, mgens); - }); - if (matchIdx === -1 && - (qsid === typeNameIdOfSlice || qsid === typeNameIdOfArray) && - queryContainsArrayOrSliceElem - ) { - currentQueryElemList = queryElemSet.get(typeNameIdOfArrayOrSlice) || []; - matchIdx = currentQueryElemList.findIndex(queryElem => { - return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem, whereClause, mgens); - }); - } - // None of the query elems match the function type. - if (matchIdx === -1) { - unbox(fnType); - return; - } - let currentFnTypeList; - if (fnTypeSet.has(fnType.id)) { - currentFnTypeList = fnTypeSet.get(fnType.id); - } else { - currentFnTypeList = []; - fnTypeSet.set(fnType.id, currentFnTypeList); - } - currentFnTypeList.push(fnType); - }; - for (const fnType of fnTypes) { - addFnTypeToFnTypeSet(fnType); - } - const doHandleQueryElemList = (currentFnTypeList, queryElemList) => { - if (queryElemList.length === 0) { return true; } - // Multiple items in one list might match multiple items in another. - // Since an item with fewer generics can match an item with more, we - // need to check all combinations for a potential match. - const queryElem = queryElemList.pop(); - const l = currentFnTypeList.length; - for (let i = 0; i < l; i += 1) { - const fnType = currentFnTypeList[i]; - if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { - continue; - } - const queryElemPathLength = queryElem.pathWithoutLast.length; - // If the query element is a path (it contains `::`), we need to check if this - // path is compatible with the target type. - if (queryElemPathLength > 0) { - const fnTypePath = fnType.path !== undefined && fnType.path !== null ? - fnType.path.split("::") : []; - // If the path provided in the query element is longer than this type, - // no need to check it since it won't match in any case. - if (queryElemPathLength > fnTypePath.length) { - continue; + return false; + }; + for (i = 0; i !== ql; ++i) { + const queryElem = queryElems[i]; + /** + * list of potential function types that go with the current query element. + * @type Array + */ + const matchCandidates = []; + let fnTypesScratch = null; + let mgensScratch = null; + // don't try anything before `i`, because they've already been + // paired off with the other query elements + for (j = i; j !== fl; ++j) { + const fnType = fnTypes[j]; + if (unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) { + if (!fnTypesScratch) { + fnTypesScratch = fnTypes.slice(); } - let i = 0; - for (const path of fnTypePath) { - if (path === queryElem.pathWithoutLast[i]) { - i += 1; - if (i >= queryElemPathLength) { - break; - } + unifyFunctionTypes( + fnType.generics, + queryElem.generics, + whereClause, + mgens, + mgensScratch => { + matchCandidates.push({ + fnTypesScratch, + mgensScratch, + queryElemsOffset: i, + fnTypesOffset: j, + unbox: false, + }); + return false; // "reject" all candidates to gather all of them } + ); + } + if (unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) { + if (!fnTypesScratch) { + fnTypesScratch = fnTypes.slice(); } - if (i < queryElemPathLength) { - // If we didn't find all parts of the path of the query element inside - // the fn type, then it's not the right one. - continue; + if (!mgensScratch) { + mgensScratch = new Map(mgens); } + backtracking.push({ + fnTypesScratch, + mgensScratch, + queryElemsOffset: i, + fnTypesOffset: j, + unbox: true, + }); } - if (queryElem.generics.length === 0 - || checkGenerics(fnType, queryElem, whereClause, mgens) - ) { - currentFnTypeList.splice(i, 1); - const result = doHandleQueryElemList(currentFnTypeList, queryElemList); - if (result) { - return true; - } - currentFnTypeList.splice(i, 0, fnType); + } + if (matchCandidates.length === 0) { + if (backtrack()) { + continue; + } else { + return false; } } + // use the current candidate + const {fnTypesOffset: candidate, mgensScratch: mgensNew} = matchCandidates.pop(); + if (fnTypes[candidate].id < 0 && queryElems[i].id < 0) { + mgens.set(fnTypes[candidate].id, queryElems[i].id); + } + for (const [fid, qid] of mgensNew) { + mgens.set(fid, qid); + } + // `i` and `j` are paired off + // `queryElems[i]` is left in place + // `fnTypes[j]` is swapped with `fnTypes[i]` to pair them off + const tmp = fnTypes[candidate]; + fnTypes[candidate] = fnTypes[i]; + fnTypes[i] = tmp; + // write other candidates to backtracking queue + for (const otherCandidate of matchCandidates) { + backtracking.push(otherCandidate); + } + // If we're on the last item, check the solution with the callback + // backtrack if the callback says its unsuitable + while (i === (ql - 1) && solutionCb && !solutionCb(mgens)) { + if (!backtrack()) { + return false; + } + } + } + return true; + } + function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens) { + // type filters look like `trait:Read` or `enum:Result` + if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { return false; - }; - /** - * @param {number} id - * @param {[QueryElement]} queryElemList - */ - const handleQueryElemList = (id, queryElemList) => { - let fsid = id; - if (fsid < 0) { - fsid = null; - if (!mgens) { + } + // 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 + // and should make that same decision everywhere it appears + if (fnType.id < 0 && queryElem.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) { + return false; + } + for (const [fid, qid] of mgens.entries()) { + if (fnType.id !== fid && queryElem.id === qid) { return false; } - for (const [fid, qsid] of mgens) { - if (id === qsid) { - fsid = fid; - break; - } + if (fnType.id === fid && queryElem.id !== qid) { + return false; } } - if (fsid === null || !fnTypeSet.has(fsid)) { - if (fsid === typeNameIdOfArrayOrSlice) { - return handleQueryElemList(typeNameIdOfSlice, queryElemList) || - handleQueryElemList(typeNameIdOfArray, queryElemList); - } + } else if (fnType.id !== null) { + if (queryElem.id === typeNameIdOfArrayOrSlice && + (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) + ) { + // [] matches primitive:array or primitive:slice + // if it matches, then we're fine, and this is an appropriate match candidate + } else if (fnType.id !== queryElem.id) { return false; } - const currentFnTypeList = fnTypeSet.get(fsid); - if (currentFnTypeList.length < queryElemList.length) { - // It's not possible for all the query elems to find a match. + // If the query elem has generics, and the function doesn't, + // it can't match. + if (fnType.generics.length === 0 && queryElem.generics.length !== 0) { return false; } - const result = doHandleQueryElemList(currentFnTypeList, queryElemList); - if (result) { - // Found a solution. - // Any items that weren't used for it can be unboxed, and might form - // part of the solution for another item. - for (const innerFnType of currentFnTypeList) { - unbox(innerFnType); + // If the query element is a path (it contains `::`), we need to check if this + // path is compatible with the target type. + const queryElemPathLength = queryElem.pathWithoutLast.length; + if (queryElemPathLength > 0) { + const fnTypePath = fnType.path !== undefined && fnType.path !== null ? + fnType.path.split("::") : []; + // If the path provided in the query element is longer than this type, + // no need to check it since it won't match in any case. + if (queryElemPathLength > fnTypePath.length) { + return false; } - fnTypeSet.delete(fsid); - } - return result; - }; - let queryElemSetSize = Number.MAX_VALUE; - while (queryElemSetSize > queryElemSet.size) { - queryElemSetSize = queryElemSet.size; - for (const [id, queryElemList] of queryElemSet) { - if (handleQueryElemList(id, queryElemList)) { - queryElemSet.delete(id); + let i = 0; + for (const path of fnTypePath) { + if (path === queryElem.pathWithoutLast[i]) { + i += 1; + if (i >= queryElemPathLength) { + break; + } + } + } + if (i < queryElemPathLength) { + // If we didn't find all parts of the path of the query element inside + // the fn type, then it's not the right one. + return false; } } } - if (queryElemSetSize === 0) { - for (const [fid, qid] of mgens) { - mgensIn.set(fid, qid); + return true; + } + function unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens) { + if (fnType.id < 0 && queryElem.id >= 0) { + if (!whereClause) { + return false; } - return true; - } else { - return false; + // 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.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 + // for example, searching `Read -> usize` should find + // `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); } + return false; } /** @@ -1907,23 +1951,19 @@ function initSearch(rawSearchIndex) { } // If the result is too "bad", we return false and it ends this search. - let mgens; - if (row.type.where_clause && row.type.where_clause.length > 0) { - mgens = new Map(); - } if (!unifyFunctionTypes( row.type.inputs, parsedQuery.elems, row.type.where_clause, - mgens - )) { - return; - } - if (!unifyFunctionTypes( - row.type.output, - parsedQuery.returned, - row.type.where_clause, - mgens + null, + mgens => { + return unifyFunctionTypes( + row.type.output, + parsedQuery.returned, + row.type.where_clause, + mgens + ); + } )) { return; } @@ -2060,7 +2100,7 @@ function initSearch(rawSearchIndex) { in_returned = row.type && unifyFunctionTypes( row.type.output, parsedQuery.returned, - row.type.where_clause, new Map() + row.type.where_clause ); if (in_returned) { addIntoResults( diff --git a/tests/rustdoc-js-std/option-type-signatures.js b/tests/rustdoc-js-std/option-type-signatures.js index 37bb3b14ab381..e154fa707ab3d 100644 --- a/tests/rustdoc-js-std/option-type-signatures.js +++ b/tests/rustdoc-js-std/option-type-signatures.js @@ -1,3 +1,5 @@ +// ignore-order + const FILTER_CRATE = "std"; const EXPECTED = [ @@ -36,8 +38,9 @@ const EXPECTED = [ ], }, { - 'query': 'option, option -> option', + 'query': 'option, option -> option', 'others': [ + { 'path': 'std::option::Option', 'name': 'and' }, { 'path': 'std::option::Option', 'name': 'zip' }, ], }, diff --git a/tests/rustdoc-js-std/vec-type-signatures.js b/tests/rustdoc-js-std/vec-type-signatures.js new file mode 100644 index 0000000000000..18cf9d6efd0fd --- /dev/null +++ b/tests/rustdoc-js-std/vec-type-signatures.js @@ -0,0 +1,22 @@ +// ignore-order + +const FILTER_CRATE = "std"; + +const EXPECTED = [ + { + 'query': 'vec::intoiter -> [T]', + 'others': [ + { 'path': 'std::vec::IntoIter', 'name': 'as_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'as_mut_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'next_chunk' }, + ], + }, + { + 'query': 'vec::intoiter -> []', + 'others': [ + { 'path': 'std::vec::IntoIter', 'name': 'as_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'as_mut_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'next_chunk' }, + ], + }, +]; diff --git a/tests/rustdoc-js/generics-match-ambiguity.js b/tests/rustdoc-js/generics-match-ambiguity.js index a9932a16ca38b..edce4268c5ac7 100644 --- a/tests/rustdoc-js/generics-match-ambiguity.js +++ b/tests/rustdoc-js/generics-match-ambiguity.js @@ -79,6 +79,7 @@ const EXPECTED = [ { 'path': 'generics_match_ambiguity', 'name': 'baac' }, { 'path': 'generics_match_ambiguity', 'name': 'baaf' }, { 'path': 'generics_match_ambiguity', 'name': 'baag' }, + { 'path': 'generics_match_ambiguity', 'name': 'baah' }, ], }, { diff --git a/tests/rustdoc-js/generics-unbox.js b/tests/rustdoc-js/generics-unbox.js new file mode 100644 index 0000000000000..9cdfc7ac8b6e5 --- /dev/null +++ b/tests/rustdoc-js/generics-unbox.js @@ -0,0 +1,38 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'Inside -> Out1', + 'others': [ + { 'path': 'generics_unbox', 'name': 'alpha' }, + ], + }, + { + 'query': 'Inside -> Out3', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out4', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out3', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out4', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, +]; diff --git a/tests/rustdoc-js/generics-unbox.rs b/tests/rustdoc-js/generics-unbox.rs new file mode 100644 index 0000000000000..bef34f891e95c --- /dev/null +++ b/tests/rustdoc-js/generics-unbox.rs @@ -0,0 +1,36 @@ +pub struct Out { + a: A, + b: B, +} + +pub struct Out1 { + a: [A; N], +} + +pub struct Out2 { + a: [A; N], +} + +pub struct Out3 { + a: A, + b: B, +} + +pub struct Out4 { + a: A, + b: B, +} + +pub struct Inside(T); + +pub fn alpha(_: Inside) -> Out, Out2> { + loop {} +} + +pub fn beta(_: Inside) -> Out, Out4> { + loop {} +} + +pub fn gamma(_: Inside) -> Out, Out4> { + loop {} +} From 4cf06e84abeb8bfc14fd7e9055db1c8ccf82266d Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 9 Sep 2023 16:59:34 -0700 Subject: [PATCH 9/9] rustdoc-doc: add `next_chunk` to list of `vec::intoiter -> [T]` This didn't show up before, because of some unification bugs that were fixed in 269cb579479ab950e85a2e4078810501c29d7465 --- src/doc/rustdoc/src/read-documentation/search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doc/rustdoc/src/read-documentation/search.md b/src/doc/rustdoc/src/read-documentation/search.md index d794dbbf250c1..56a5016d0cee0 100644 --- a/src/doc/rustdoc/src/read-documentation/search.md +++ b/src/doc/rustdoc/src/read-documentation/search.md @@ -71,7 +71,7 @@ the standard library and functions that are included in the results list: | [`option -> default`][] | `Option::unwrap_or_default` | | [`stdout, [u8]`][stdoutu8] | `Stdout::write` | | [`any -> !`][] | `panic::panic_any` | -| [`vec::intoiter -> [T]`][iterasslice] | `IntoIter::as_slice` | +| [`vec::intoiter -> [T]`][iterasslice] | `IntoIter::as_slice` and `IntoIter::next_chunk` | [`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