From 2e569274d3ecbbc74d9c5b3a89a68ee09c19ef2f Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 25 May 2023 12:58:36 -0700 Subject: [PATCH] rustdoc: search for slices and arrays by type with `[]` Part of #60485 --- src/librustdoc/html/static/js/search.js | 233 ++++++++----- .../rustdoc-js-std/option-type-signatures.js | 7 + tests/rustdoc-js-std/parser-slice-array.js | 308 ++++++++++++++++++ tests/rustdoc-js/slice-array.js | 19 ++ tests/rustdoc-js/slice-array.rs | 2 + 5 files changed, 487 insertions(+), 82 deletions(-) create mode 100644 tests/rustdoc-js-std/parser-slice-array.js diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 62afe40bb3155..984358396ab2a 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -208,6 +208,46 @@ function initSearch(rawSearchIndex) { let typeNameIdMap; const ALIASES = new Map(); + /** + * Special type name IDs for searching by array. + */ + let typeNameIdOfArray; + /** + * Special type name IDs for searching by slice. + */ + let typeNameIdOfSlice; + /** + * Special type name IDs for searching by both array and slice (`[]` syntax). + */ + let typeNameIdOfArrayOrSlice; + + /** + * 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). + * + * This is effectively string interning, so that function matching can be + * done more quickly. Two types with the same name but different item kinds + * get the same ID. + * + * @param {string} name + * + * @returns {integer} + */ + function buildTypeMapIndex(name) { + + if (name === "" || name === null) { + return -1; + } + + if (typeNameIdMap.has(name)) { + return typeNameIdMap.get(name); + } else { + const id = typeNameIdMap.size; + typeNameIdMap.set(name, id); + return id; + } + } + function isWhitespace(c) { return " \t\n\r".indexOf(c) !== -1; } @@ -217,7 +257,7 @@ function initSearch(rawSearchIndex) { } function isEndCharacter(c) { - return ",>-".indexOf(c) !== -1; + return ",>-]".indexOf(c) !== -1; } function isStopCharacter(c) { @@ -466,35 +506,64 @@ function initSearch(rawSearchIndex) { let start = parserState.pos; let end; - // We handle the strings on their own mostly to make code easier to follow. - if (parserState.userQuery[parserState.pos] === "\"") { - start += 1; - getStringElem(query, parserState, isInGenerics); - end = parserState.pos - 1; + if (parserState.userQuery[parserState.pos] === "[") { + parserState.pos += 1; + getItemsBefore(query, parserState, generics, "]"); + const typeFilter = parserState.typeFilter; + if (typeFilter !== null && typeFilter !== "primitive") { + throw [ + "Invalid search type: primitive ", + "[]", + " and ", + typeFilter, + " both specified", + ]; + } + parserState.typeFilter = null; + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + elems.push({ + name: "[]", + id: -1, + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics, + typeFilter: "primitive", + }); } else { - end = getIdentEndPosition(parserState); - } - if (parserState.pos < parserState.length && - parserState.userQuery[parserState.pos] === "<" - ) { - if (start >= end) { - throw ["Found generics without a path"]; + // We handle the strings on their own mostly to make code easier to follow. + if (parserState.userQuery[parserState.pos] === "\"") { + start += 1; + getStringElem(query, parserState, isInGenerics); + end = parserState.pos - 1; + } else { + end = getIdentEndPosition(parserState); } - parserState.pos += 1; - getItemsBefore(query, parserState, generics, ">"); - } - if (start >= end && generics.length === 0) { - return; + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<" + ) { + if (start >= end) { + throw ["Found generics without a path"]; + } + parserState.pos += 1; + getItemsBefore(query, parserState, generics, ">"); + } + if (start >= end && generics.length === 0) { + return; + } + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics + ) + ); } - elems.push( - createQueryElement( - query, - parserState, - parserState.userQuery.slice(start, end), - generics, - isInGenerics - ) - ); } /** @@ -518,6 +587,17 @@ function initSearch(rawSearchIndex) { const oldTypeFilter = parserState.typeFilter; parserState.typeFilter = null; + let extra = ""; + if (endChar === ">") { + extra = "<"; + } else if (endChar === "]") { + extra = "["; + } else if (endChar === "") { + extra = "->"; + } else { + extra = endChar; + } + while (parserState.pos < parserState.length) { const c = parserState.userQuery[parserState.pos]; if (c === endChar) { @@ -547,14 +627,6 @@ function initSearch(rawSearchIndex) { foundStopChar = true; continue; } else if (isEndCharacter(c)) { - let extra = ""; - if (endChar === ">") { - extra = "<"; - } else if (endChar === "") { - extra = "->"; - } else { - extra = endChar; - } throw ["Unexpected ", c, " after ", extra]; } if (!foundStopChar) { @@ -581,9 +653,9 @@ function initSearch(rawSearchIndex) { } const posBefore = parserState.pos; start = parserState.pos; - getNextElem(query, parserState, elems, endChar === ">"); + getNextElem(query, parserState, elems, endChar !== ""); if (endChar !== "" && parserState.pos >= parserState.length) { - throw ["Unclosed ", "<"]; + throw ["Unclosed ", extra]; } // This case can be encountered if `getNextElem` encountered a "stop character" right // from the start. For example if you have `,,` or `<>`. In this case, we simply move up @@ -594,7 +666,7 @@ function initSearch(rawSearchIndex) { foundStopChar = false; } if (parserState.pos >= parserState.length && endChar !== "") { - throw ["Unclosed ", "<"]; + throw ["Unclosed ", extra]; } // We are either at the end of the string or on the `endChar` character, let's move forward // in any case. @@ -779,7 +851,8 @@ function initSearch(rawSearchIndex) { * * ident = *(ALPHA / DIGIT / "_") * path = ident *(DOUBLE-COLON ident) [!] - * arg = [type-filter *WS COLON *WS] path [generics] + * 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) @@ -821,6 +894,8 @@ function initSearch(rawSearchIndex) { * * OPEN-ANGLE-BRACKET = "<" * CLOSE-ANGLE-BRACKET = ">" + * OPEN-SQUARE-BRACKET = "[" + * CLOSE-SQUARE-BRACKET = "]" * COLON = ":" * DOUBLE-COLON = "::" * QUOTE = %x22 @@ -1170,7 +1245,22 @@ function initSearch(rawSearchIndex) { // ones with no type filter, which can match any entry regardless of its // own type. for (const generic of elem.generics) { - if (generic.typeFilter !== -1 && !handleGeneric(generic)) { + if (generic.typeFilter === TY_PRIMITIVE && + generic.id === typeNameIdOfArrayOrSlice) { + const genericArray = { + id: typeNameIdOfArray, + typeFilter: TY_PRIMITIVE, + generics: generic.generics, + }; + const genericSlice = { + id: typeNameIdOfSlice, + typeFilter: TY_PRIMITIVE, + generics: generic.generics, + }; + if (!handleGeneric(genericArray) && !handleGeneric(genericSlice)) { + return false; + } + } else if (generic.typeFilter !== -1 && !handleGeneric(generic)) { return false; } } @@ -1217,7 +1307,12 @@ function initSearch(rawSearchIndex) { return row.generics.length > 0 ? checkIfInGenerics(row, elem) : false; } - if (row.id === elem.id && typePassesFilter(elem.typeFilter, row.ty)) { + const matchesExact = row.id === elem.id; + const matchesArrayOrSlice = elem.id === typeNameIdOfArrayOrSlice && + (row.id === typeNameIdOfSlice || row.id === typeNameIdOfArray); + + if ((matchesExact || matchesArrayOrSlice) && + typePassesFilter(elem.typeFilter, row.ty)) { if (elem.generics.length > 0) { return checkGenerics(row, elem); } @@ -2082,34 +2177,6 @@ function initSearch(rawSearchIndex) { filterCrates); } - /** - * 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). - * - * This is effectively string interning, so that function matching can be - * done more quickly. Two types with the same name but different item kinds - * get the same ID. - * - * @param {Map} typeNameIdMap - * @param {string} name - * - * @returns {integer} - */ - function buildTypeMapIndex(typeNameIdMap, name) { - - if (name === "" || name === null) { - return -1; - } - - if (typeNameIdMap.has(name)) { - return typeNameIdMap.get(name); - } else { - const id = typeNameIdMap.size; - typeNameIdMap.set(name, id); - return id; - } - } - /** * Convert a list of RawFunctionType / ID to object-based FunctionType. * @@ -2128,7 +2195,7 @@ function initSearch(rawSearchIndex) { * * @return {Array} */ - function buildItemSearchTypeAll(types, lowercasePaths, typeNameIdMap) { + function buildItemSearchTypeAll(types, lowercasePaths) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; return types.map(type => { @@ -2140,15 +2207,14 @@ function initSearch(rawSearchIndex) { pathIndex = type[PATH_INDEX_DATA]; generics = buildItemSearchTypeAll( type[GENERICS_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } return { // `0` is used as a sentinel because it's fewer bytes than `null` id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: generics, }; @@ -2171,7 +2237,7 @@ function initSearch(rawSearchIndex) { * * @return {null|FunctionSearchType} */ - function buildFunctionSearchType(functionSearchType, lowercasePaths, typeNameIdMap) { + function buildFunctionSearchType(functionSearchType, lowercasePaths) { const INPUTS_DATA = 0; const OUTPUT_DATA = 1; // `0` is used as a sentinel because it's fewer bytes than `null` @@ -2184,15 +2250,14 @@ function initSearch(rawSearchIndex) { inputs = [{ id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: [], }]; } else { inputs = buildItemSearchTypeAll( functionSearchType[INPUTS_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } if (functionSearchType.length > 1) { @@ -2201,15 +2266,14 @@ function initSearch(rawSearchIndex) { output = [{ id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: [], }]; } else { output = buildItemSearchTypeAll( functionSearchType[OUTPUT_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } } else { @@ -2233,6 +2297,12 @@ function initSearch(rawSearchIndex) { let currentIndex = 0; let id = 0; + // Initialize type map indexes for primitive list types + // that can be searched using `[]` syntax. + typeNameIdOfArray = buildTypeMapIndex("array"); + typeNameIdOfSlice = buildTypeMapIndex("slice"); + typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]"); + for (const crate in rawSearchIndex) { if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) { continue; @@ -2363,8 +2433,7 @@ function initSearch(rawSearchIndex) { parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined, type: buildFunctionSearchType( itemFunctionSearchTypes[i], - lowercasePaths, - typeNameIdMap + lowercasePaths ), id: id, normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), diff --git a/tests/rustdoc-js-std/option-type-signatures.js b/tests/rustdoc-js-std/option-type-signatures.js index 8f6b0450dd319..2599785066145 100644 --- a/tests/rustdoc-js-std/option-type-signatures.js +++ b/tests/rustdoc-js-std/option-type-signatures.js @@ -12,4 +12,11 @@ const EXPECTED = [ { 'path': 'std::option::Option', 'name': 'get_or_insert_default' }, ], }, + { + 'query': 'option -> []', + 'others': [ + { 'path': 'std::option::Option', 'name': 'as_slice' }, + { 'path': 'std::option::Option', 'name': 'as_mut_slice' }, + ], + }, ]; diff --git a/tests/rustdoc-js-std/parser-slice-array.js b/tests/rustdoc-js-std/parser-slice-array.js new file mode 100644 index 0000000000000..4c0a1defe7105 --- /dev/null +++ b/tests/rustdoc-js-std/parser-slice-array.js @@ -0,0 +1,308 @@ +const QUERY = [ + '[[[D, []]]', + '[[[D, []]]]', + '[] u8', + '[u8]', + '[u8,u8]', + '[u8]', + '[]', + '[>', + '[<', + '[a>', + '[a<', + '[a', + '[', + ']', + 'primitive:[u8]', + 'macro:[u8]', +]; + +const PARSED = [ + { + elems: [], + foundElems: 0, + original: '[[[D, []]]', + returned: [], + userQuery: '[[[d, []]]', + error: 'Unclosed `[`', + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "d", + fullPath: ["d"], + pathWithoutLast: [], + pathLast: "d", + generics: [], + typeFilter: -1, + }, + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [], + typeFilter: 15, + }, + ], + typeFilter: 15, + }, + ], + typeFilter: 15, + }, + ], + typeFilter: 15, + }, + ], + foundElems: 1, + original: '[[[D, []]]]', + returned: [], + userQuery: '[[[d, []]]]', + error: null, + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [], + typeFilter: 15, + }, + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + ], + foundElems: 2, + original: "[] u8", + returned: [], + userQuery: "[] u8", + error: null, + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + ], + typeFilter: 15, + }, + ], + foundElems: 1, + original: "[u8]", + returned: [], + userQuery: "[u8]", + error: null, + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + ], + typeFilter: 15, + }, + ], + foundElems: 1, + original: "[u8,u8]", + returned: [], + userQuery: "[u8,u8]", + error: null, + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [ + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + ], + typeFilter: -1, + }, + ], + typeFilter: 15, + }, + ], + foundElems: 1, + original: "[u8]", + returned: [], + userQuery: "[u8]", + error: null, + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [], + typeFilter: 15, + }, + ], + foundElems: 1, + original: "[]", + returned: [], + userQuery: "[]", + error: null, + }, + { + elems: [], + foundElems: 0, + original: "[>", + returned: [], + userQuery: "[>", + error: "Unexpected `>` after `[`", + }, + { + elems: [], + foundElems: 0, + original: "[<", + returned: [], + userQuery: "[<", + error: "Found generics without a path", + }, + { + elems: [], + foundElems: 0, + original: "[a>", + returned: [], + userQuery: "[a>", + error: "Unexpected `>` after `[`", + }, + { + elems: [], + foundElems: 0, + original: "[a<", + returned: [], + userQuery: "[a<", + error: "Unclosed `<`", + }, + { + elems: [], + foundElems: 0, + original: "[a", + returned: [], + userQuery: "[a", + error: "Unclosed `[`", + }, + { + elems: [], + foundElems: 0, + original: "[", + returned: [], + userQuery: "[", + error: "Unclosed `[`", + }, + { + elems: [], + foundElems: 0, + original: "]", + returned: [], + userQuery: "]", + error: "Unexpected `]`", + }, + { + elems: [ + { + name: "[]", + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics: [ + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + typeFilter: -1, + }, + ], + typeFilter: 15, + }, + ], + foundElems: 1, + original: "primitive:[u8]", + returned: [], + userQuery: "primitive:[u8]", + error: null, + }, + { + elems: [], + foundElems: 0, + original: "macro:[u8]", + returned: [], + userQuery: "macro:[u8]", + error: "Invalid search type: primitive `[]` and `macro` both specified", + }, +]; diff --git a/tests/rustdoc-js/slice-array.js b/tests/rustdoc-js/slice-array.js index 1c06566920c2b..83b9f69d03e90 100644 --- a/tests/rustdoc-js/slice-array.js +++ b/tests/rustdoc-js/slice-array.js @@ -51,4 +51,23 @@ const EXPECTED = [ { 'path': 'slice_array', 'name': 'gamma' }, ], }, + { + 'query': '[TraitDog]', + 'in_args': [ + { 'path': 'slice_array', 'name': 'gamma' }, + { 'path': 'slice_array', 'name': 'epsilon' }, + ], + }, + { + 'query': 'R<[Q]>', + 'returned': [ + { 'path': 'slice_array', 'name': 'bet' }, + ], + }, + { + 'query': 'R<[P]>', + 'in_args': [ + { 'path': 'slice_array', 'name': 'alpha' }, + ], + }, ]; diff --git a/tests/rustdoc-js/slice-array.rs b/tests/rustdoc-js/slice-array.rs index 2523b21cfaa50..15ac4294f3d79 100644 --- a/tests/rustdoc-js/slice-array.rs +++ b/tests/rustdoc-js/slice-array.rs @@ -14,3 +14,5 @@ pub trait TraitCat {} pub trait TraitDog {} pub fn gamma(t: [T; 32]) {} + +pub fn epsilon(t: &[T]) {}