diff options
| author | Dylan DPC <99973273+Dylan-DPC@users.noreply.github.com> | 2022-04-21 01:14:13 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-04-21 01:14:13 +0200 |
| commit | 976c6b2d193148ca9df3a505e55c5ba5da22cd96 (patch) | |
| tree | e190b90c0d06101df0a683fdc5b5fc339c47349a | |
| parent | 51ea9bb29b07d76c5a7167d054b54f4eb7f5b44e (diff) | |
| parent | 4d26bde4f0b24ca6121eec167bc8d48f4b7179cc (diff) | |
| download | rust-976c6b2d193148ca9df3a505e55c5ba5da22cd96.tar.gz rust-976c6b2d193148ca9df3a505e55c5ba5da22cd96.zip | |
Rollup merge of #90630 - GuillaumeGomez:improve-rustdoc-search, r=notriddle
Create real parser for search queries You can test it [here](https://rustdoc.crud.net/imperio/improve-rustdoc-search/std/index.html). This PR adds a real parser for the query engine in rustdoc. The parser is quite simple but it allows to makes query handling much easier. I added a new testsuite to ensure it works as expected and ran fuzzing checks on it for a few hours without problems. So about the parser: as you can see in the screenshot, it handles recursive generics parsing. It also allows to set which item should use exact matching by adding double-quotes around it (look for `exact_search` in the screenshot). Now about the query engine itself: I simplified it a lot thanks to the parsed query. It behaves mostly the same when there is only one argument, but is much more powerful when there are more than one. When making this change, we also removed the support for multi-query. PS: A big part of the PR is tests and test-related code. :) r? `@camelid`
21 files changed, 2299 insertions, 457 deletions
diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index 629f90728d2..0fe0fdadbd2 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -8,10 +8,34 @@ function initSearch(searchIndex){} /** * @typedef {{ - * raw: string, - * query: string, - * type: string, - * id: string, + * name: string, + * fullPath: Array<string>, + * pathWithoutLast: Array<string>, + * pathLast: string, + * generics: Array<QueryElement>, + * }} + */ +var QueryElement; + +/** + * @typedef {{ + * pos: number, + * totalElems: number, + * typeFilter: (null|string), + * userQuery: string, + * }} + */ +var ParserState; + +/** + * @typedef {{ + * original: string, + * userQuery: string, + * typeFilter: number, + * elems: Array<QueryElement>, + * args: Array<QueryElement>, + * returned: Array<QueryElement>, + * foundElems: number, * }} */ var ParsedQuery; @@ -30,3 +54,30 @@ var ParsedQuery; * }} */ var Row; + +/** + * @typedef {{ + * in_args: Array<Object>, + * returned: Array<Object>, + * others: Array<Object>, + * query: ParsedQuery, + * }} + */ +var ResultsTable; + +/** + * @typedef {{ + * desc: string, + * displayPath: string, + * fullPath: string, + * href: string, + * id: number, + * lev: number, + * name: string, + * normalizedName: string, + * parent: (Object|undefined), + * path: string, + * ty: number, + * }} + */ +var Results; diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index ab52304491a..0d4e0a0b328 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -61,15 +61,6 @@ function printTab(nb) { }); } -function removeEmptyStringsFromArray(x) { - for (var i = 0, len = x.length; i < len; ++i) { - if (x[i] === "") { - x.splice(i, 1); - i -= 1; - } - } -} - /** * A function to compute the Levenshtein distance between two strings * Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported @@ -133,11 +124,436 @@ window.initSearch = function(rawSearchIndex) { searchState.input.value = params.search || ""; } + function isWhitespace(c) { + return " \t\n\r".indexOf(c) !== -1; + } + + function isSpecialStartCharacter(c) { + return "<\"".indexOf(c) !== -1; + } + + function isEndCharacter(c) { + return ",>-".indexOf(c) !== -1; + } + + function isStopCharacter(c) { + return isWhitespace(c) || isEndCharacter(c); + } + + function isErrorCharacter(c) { + return "()".indexOf(c) !== -1; + } + + function itemTypeFromName(typename) { + for (var i = 0, len = itemTypes.length; i < len; ++i) { + if (itemTypes[i] === typename) { + return i; + } + } + + throw new Error("Unknown type filter `" + typename + "`"); + } + + /** + * If we encounter a `"`, then we try to extract the string from it until we find another `"`. + * + * This function will throw an error in the following cases: + * * There is already another string element. + * * We are parsing a generic argument. + * * There is more than one element. + * * There is no closing `"`. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {boolean} isInGenerics + */ + function getStringElem(query, parserState, isInGenerics) { + if (isInGenerics) { + throw new Error("`\"` cannot be used in generics"); + } else if (query.literalSearch) { + throw new Error("Cannot have more than one literal search element"); + } else if (parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("Cannot use literal search when there is more than one element"); + } + parserState.pos += 1; + var start = parserState.pos; + var end = getIdentEndPosition(parserState); + if (parserState.pos >= parserState.length) { + throw new Error("Unclosed `\"`"); + } else if (parserState.userQuery[end] !== "\"") { + throw new Error(`Unexpected \`${parserState.userQuery[end]}\` in a string element`); + } else if (start === end) { + throw new Error("Cannot have empty string element"); + } + // To skip the quote at the end. + parserState.pos += 1; + query.literalSearch = true; + } + + /** + * Returns `true` if the current parser position is starting with "::". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isPathStart(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) == '::'; + } + + /** + * Returns `true` if the current parser position is starting with "->". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isReturnArrow(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) == '->'; + } + + /** + * Returns `true` if the given `c` character is valid for an ident. + * + * @param {string} c + * + * @return {boolean} + */ + function isIdentCharacter(c) { + return ( + c === '_' || + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z')); + } + + /** + * Returns `true` if the given `c` character is a separator. + * + * @param {string} c + * + * @return {boolean} + */ + function isSeparatorCharacter(c) { + return c === "," || isWhitespaceCharacter(c); + } + + /** + * Returns `true` if the given `c` character is a whitespace. + * + * @param {string} c + * + * @return {boolean} + */ + function isWhitespaceCharacter(c) { + return c === " " || c === "\t"; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {string} name - Name of the query element. + * @param {Array<QueryElement>} generics - List of generics of this query element. + * + * @return {QueryElement} - The newly created `QueryElement`. + */ + function createQueryElement(query, parserState, name, generics, isInGenerics) { + if (name === '*' || (name.length === 0 && generics.length === 0)) { + return; + } + if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("You cannot have more than one element if you use quotes"); + } + var pathSegments = name.split("::"); + if (pathSegments.length > 1) { + for (var i = 0, len = pathSegments.length; i < len; ++i) { + var pathSegment = pathSegments[i]; + + if (pathSegment.length === 0) { + if (i === 0) { + throw new Error("Paths cannot start with `::`"); + } else if (i + 1 === len) { + throw new Error("Paths cannot end with `::`"); + } + throw new Error("Unexpected `::::`"); + } + } + } + // In case we only have something like `<p>`, there is no name. + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { + throw new Error("Found generics without a path"); + } + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + return { + name: name, + fullPath: pathSegments, + pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), + pathLast: pathSegments[pathSegments.length - 1], + generics: generics, + }; + } + + /** + * This function goes through all characters until it reaches an invalid ident character or the + * end of the query. It returns the position of the last character of the ident. + * + * @param {ParserState} parserState + * + * @return {integer} + */ + function getIdentEndPosition(parserState) { + var end = parserState.pos; + while (parserState.pos < parserState.length) { + var c = parserState.userQuery[parserState.pos]; + if (!isIdentCharacter(c)) { + if (isErrorCharacter(c)) { + throw new Error(`Unexpected \`${c}\``); + } else if ( + isStopCharacter(c) || + isSpecialStartCharacter(c) || + isSeparatorCharacter(c)) + { + break; + } + // If we allow paths ("str::string" for example). + else if (c === ":") { + if (!isPathStart(parserState)) { + break; + } + // Skip current ":". + parserState.pos += 1; + } else { + throw new Error(`Unexpected \`${c}\``); + } + } + parserState.pos += 1; + end = parserState.pos; + } + return end; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added. + * @param {boolean} isInGenerics + */ + function getNextElem(query, parserState, elems, isInGenerics) { + var generics = []; + + var start = parserState.pos; + var 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; + } else { + end = getIdentEndPosition(parserState); + } + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<") + { + if (isInGenerics) { + throw new Error("Unexpected `<` after `<`"); + } else if (start >= end) { + throw new Error("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 + ) + ); + } + + /** + * This function parses the next query element until it finds `endChar`, calling `getNextElem` + * to collect each element. + * + * If there is no `endChar`, this function will implicitly stop at the end without raising an + * error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added. + * @param {string} endChar - This function will stop when it'll encounter this + * character. + */ + function getItemsBefore(query, parserState, elems, endChar) { + var foundStopChar = true; + + while (parserState.pos < parserState.length) { + var c = parserState.userQuery[parserState.pos]; + if (c === endChar) { + break; + } else if (isSeparatorCharacter(c)) { + parserState.pos += 1; + foundStopChar = true; + continue; + } else if (c === ":" && isPathStart(parserState)) { + throw new Error("Unexpected `::`: paths cannot start with `::`"); + } else if (c === ":" || isEndCharacter(c)) { + var extra = ""; + if (endChar === ">") { + extra = "`<`"; + } else if (endChar === "") { + extra = "`->`"; + } + throw new Error("Unexpected `" + c + "` after " + extra); + } + if (!foundStopChar) { + if (endChar !== "") { + throw new Error(`Expected \`,\`, \` \` or \`${endChar}\`, found \`${c}\``); + } + throw new Error(`Expected \`,\` or \` \`, found \`${c}\``); + } + var posBefore = parserState.pos; + getNextElem(query, parserState, elems, endChar === ">"); + // This case can be encountered if `getNextElem` encounted a "stop character" right from + // the start. For example if you have `,,` or `<>`. In this case, we simply move up the + // current position to continue the parsing. + if (posBefore === parserState.pos) { + parserState.pos += 1; + } + foundStopChar = false; + } + // We are either at the end of the string or on the `endChar`` character, let's move forward + // in any case. + parserState.pos += 1; + } + + /** + * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored + * if empty). + * + * @param {ParserState} parserState + */ + function checkExtraTypeFilterCharacters(parserState) { + var query = parserState.userQuery; + + for (var pos = 0; pos < parserState.pos; ++pos) { + if (!isIdentCharacter(query[pos]) && !isWhitespaceCharacter(query[pos])) { + throw new Error(`Unexpected \`${query[pos]}\` in type filter`); + } + } + } + + /** + * Parses the provided `query` input to fill `parserState`. If it encounters an error while + * parsing `query`, it'll throw an error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + */ + function parseInput(query, parserState) { + var c, before; + var foundStopChar = true; + + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isStopCharacter(c)) { + foundStopChar = true; + if (isSeparatorCharacter(c)) { + parserState.pos += 1; + continue; + } else if (c === "-" || c === ">") { + if (isReturnArrow(parserState)) { + break; + } + throw new Error(`Unexpected \`${c}\` (did you mean \`->\`?)`); + } + throw new Error(`Unexpected \`${c}\``); + } else if (c === ":" && !isPathStart(parserState)) { + if (parserState.typeFilter !== null) { + throw new Error("Unexpected `:`"); + } + if (query.elems.length === 0) { + throw new Error("Expected type filter before `:`"); + } else if (query.elems.length !== 1 || parserState.totalElems !== 1) { + throw new Error("Unexpected `:`"); + } else if (query.literalSearch) { + throw new Error("You cannot use quotes on type filter"); + } + checkExtraTypeFilterCharacters(parserState); + // The type filter doesn't count as an element since it's a modifier. + parserState.typeFilter = query.elems.pop().name; + parserState.pos += 1; + parserState.totalElems = 0; + query.literalSearch = false; + foundStopChar = true; + continue; + } + if (!foundStopChar) { + if (parserState.typeFilter !== null) { + throw new Error(`Expected \`,\`, \` \` or \`->\`, found \`${c}\``); + } + throw new Error(`Expected \`,\`, \` \`, \`:\` or \`->\`, found \`${c}\``); + } + before = query.elems.length; + getNextElem(query, parserState, query.elems, false); + if (query.elems.length === before) { + // Nothing was added, weird... Let's increase the position to not remain stuck. + parserState.pos += 1; + } + foundStopChar = false; + } + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isReturnArrow(parserState)) { + parserState.pos += 2; + // Get returned elements. + getItemsBefore(query, parserState, query.returned, ""); + // Nothing can come afterward! + if (query.returned.length === 0) { + throw new Error("Expected at least one item after `->`"); + } + break; + } else { + parserState.pos += 1; + } + } + } + + /** + * Takes the user search input and returns an empty `ParsedQuery`. + * + * @param {string} userQuery + * + * @return {ParsedQuery} + */ + function newParsedQuery(userQuery) { + return { + original: userQuery, + userQuery: userQuery.toLowerCase(), + typeFilter: NO_TYPE_FILTER, + elems: [], + returned: [], + // Total number of "top" elements (does not include generics). + foundElems: 0, + literalSearch: false, + error: null, + }; + } + /** * Build an URL with search parameters. * * @param {string} search - The current search being performed. * @param {string|null} filterCrates - The current filtering crate (if any). + * * @return {string} */ function buildUrl(search, filterCrates) { @@ -167,33 +583,139 @@ window.initSearch = function(rawSearchIndex) { } /** - * Executes the query and returns a list of results for each results tab. - * @param {Object} query - The user query - * @param {Array<string>} searchWords - The list of search words to query against - * @param {string} [filterCrates] - Crate to search in - * @return {{ - * in_args: Array<?>, - * returned: Array<?>, - * others: Array<?> - * }} + * Parses the query. + * + * The supported syntax by this parser is as follow: + * + * ident = *(ALPHA / DIGIT / "_") + * path = ident *(DOUBLE-COLON ident) + * arg = path [generics] + * arg-without-generic = path + * type-sep = COMMA/WS *(COMMA/WS) + * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) + * nonempty-arg-list-without-generics = *(type-sep) arg-without-generic + * *(type-sep arg-without-generic) *(type-sep) + * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list-without-generics ] *(type-sep) + * CLOSE-ANGLE-BRACKET/EOF + * return-args = RETURN-ARROW *(type-sep) nonempty-arg-list + * + * exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] + * type-search = [type-filter *WS COLON] [ 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 = ">" + * COLON = ":" + * DOUBLE-COLON = "::" + * QUOTE = %x22 + * COMMA = "," + * RETURN-ARROW = "->" + * + * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + * DIGIT = %x30-39 + * WS = %x09 / " " + * + * @param {string} val - The user query + * + * @return {ParsedQuery} - The parsed query */ - function execQuery(query, searchWords, filterCrates) { - function itemTypeFromName(typename) { - for (var i = 0, len = itemTypes.length; i < len; ++i) { - if (itemTypes[i] === typename) { - return i; + function parseQuery(userQuery) { + userQuery = userQuery.trim(); + var parserState = { + length: userQuery.length, + pos: 0, + // Total number of elements (includes generics). + totalElems: 0, + genericsElems: 0, + typeFilter: null, + userQuery: userQuery.toLowerCase(), + }; + var query = newParsedQuery(userQuery); + + try { + parseInput(query, parserState); + if (parserState.typeFilter !== null) { + var typeFilter = parserState.typeFilter; + if (typeFilter === "const") { + typeFilter = "constant"; } + query.typeFilter = itemTypeFromName(typeFilter); } - return NO_TYPE_FILTER; + } catch (err) { + query = newParsedQuery(userQuery); + query.error = err.message; + query.typeFilter = -1; + return query; } - var valLower = query.query.toLowerCase(), - val = valLower, - typeFilter = itemTypeFromName(query.type), - results = {}, results_in_args = {}, results_returned = {}, - split = valLower.split("::"); + if (!query.literalSearch) { + // If there is more than one element in the query, we switch to literalSearch in any + // case. + query.literalSearch = parserState.totalElems > 1; + } + query.foundElems = query.elems.length + query.returned.length; + return query; + } - removeEmptyStringsFromArray(split); + /** + * Creates the query results. + * + * @param {Array<Result>} results_in_args + * @param {Array<Result>} results_returned + * @param {Array<Result>} results_in_args + * @param {ParsedQuery} parsedQuery + * + * @return {ResultsTable} + */ + function createQueryResults(results_in_args, results_returned, results_others, parsedQuery) { + return { + "in_args": results_in_args, + "returned": results_returned, + "others": results_others, + "query": parsedQuery, + }; + } + + /** + * Executes the parsed query and builds a {ResultsTable}. + * + * @param {ParsedQuery} parsedQuery - The parsed user query + * @param {Object} searchWords - The list of search words to query against + * @param {Object} [filterCrates] - Crate to search in if defined + * + * @return {ResultsTable} + */ + function execQuery(parsedQuery, searchWords, filterCrates) { + var results_others = {}, results_in_args = {}, results_returned = {}; function transformResults(results) { var duplicates = {}; @@ -227,6 +749,7 @@ window.initSearch = function(rawSearchIndex) { } function sortResults(results, isType) { + var userQuery = parsedQuery.userQuery; var ar = []; for (var entry in results) { if (hasOwnPropertyRustdoc(results, entry)) { @@ -246,8 +769,8 @@ window.initSearch = function(rawSearchIndex) { var a, b; // sort by exact match with regard to the last word (mismatch goes later) - a = (aaa.word !== val); - b = (bbb.word !== val); + a = (aaa.word !== userQuery); + b = (bbb.word !== userQuery); if (a !== b) { return a - b; } // Sort by non levenshtein results and then levenshtein results by the distance @@ -309,6 +832,12 @@ window.initSearch = function(rawSearchIndex) { return 0; }); + var nameSplit = null; + if (parsedQuery.elems.length === 1) { + var hasPath = typeof parsedQuery.elems[0].path === "undefined"; + nameSplit = hasPath ? null : parsedQuery.elems[0].path; + } + for (var i = 0, len = results.length; i < len; ++i) { result = results[i]; @@ -320,215 +849,222 @@ window.initSearch = function(rawSearchIndex) { path = result.item.path.toLowerCase(), parent = result.item.parent; - if (!isType && !validateResult(name, path, split, parent)) { + if (!isType && !validateResult(name, path, nameSplit, parent)) { result.id = -1; } } return transformResults(results); } - function extractGenerics(val) { - val = val.toLowerCase(); - if (val.indexOf("<") !== -1) { - var values = val.substring(val.indexOf("<") + 1, val.lastIndexOf(">")); - return { - name: val.substring(0, val.indexOf("<")), - generics: values.split(/\s*,\s*/), - }; + /** + * This function checks if the object (`row`) generics match the given type (`elem`) + * generics. If there are no generics on `row`, `defaultLev` is returned. + * + * @param {Row} row - The object to check. + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} defaultLev - This is the value to return in case there are no generics. + * + * @return {integer} - Returns the best match (if any) or `MAX_LEV_DISTANCE + 1`. + */ + function checkGenerics(row, elem, defaultLev) { + if (row.length <= GENERICS_DATA || row[GENERICS_DATA].length === 0) { + return elem.generics.length === 0 ? defaultLev : MAX_LEV_DISTANCE + 1; + } else if (row[GENERICS_DATA].length > 0 && row[GENERICS_DATA][0][NAME] === "") { + if (row.length > GENERICS_DATA) { + return checkGenerics(row[GENERICS_DATA][0], elem, defaultLev); + } + return elem.generics.length === 0 ? defaultLev : MAX_LEV_DISTANCE + 1; } - return { - name: val, - generics: [], - }; - } - - function checkGenerics(obj, val) { // The names match, but we need to be sure that all generics kinda // match as well. - var tmp_lev, elem_name; - if (val.generics.length > 0) { - if (obj.length > GENERICS_DATA && - obj[GENERICS_DATA].length >= val.generics.length) { - var elems = Object.create(null); - var elength = obj[GENERICS_DATA].length; - for (var x = 0; x < elength; ++x) { - if (!elems[obj[GENERICS_DATA][x][NAME]]) { - elems[obj[GENERICS_DATA][x][NAME]] = 0; + var elem_name; + if (elem.generics.length > 0 && row[GENERICS_DATA].length >= elem.generics.length) { + var elems = Object.create(null); + for (var x = 0, length = row[GENERICS_DATA].length; x < length; ++x) { + elem_name = row[GENERICS_DATA][x][NAME]; + if (elem_name === "") { + // Pure generic, needs to check into it. + if (checkGenerics( + row[GENERICS_DATA][x], elem, MAX_LEV_DISTANCE + 1) !== 0) { + return MAX_LEV_DISTANCE + 1; } - elems[obj[GENERICS_DATA][x][NAME]] += 1; + continue; + } + if (elems[elem_name] === undefined) { + elems[elem_name] = 0; } - var total = 0; - var done = 0; - // We need to find the type that matches the most to remove it in order - // to move forward. - var vlength = val.generics.length; - for (x = 0; x < vlength; ++x) { - var lev = MAX_LEV_DISTANCE + 1; - var firstGeneric = val.generics[x]; - var match = null; - if (elems[firstGeneric]) { - match = firstGeneric; - lev = 0; - } else { - for (elem_name in elems) { - tmp_lev = levenshtein(elem_name, firstGeneric); - if (tmp_lev < lev) { - lev = tmp_lev; - match = elem_name; - } + elems[elem_name] += 1; + } + // We need to find the type that matches the most to remove it in order + // to move forward. + for (x = 0, length = elem.generics.length; x < length; ++x) { + var generic = elem.generics[x]; + var match = null; + if (elems[generic.name]) { + match = generic.name; + } else { + for (elem_name in elems) { + if (!hasOwnPropertyRustdoc(elems, elem_name)) { + continue; } - } - if (match !== null) { - elems[match] -= 1; - if (elems[match] == 0) { - delete elems[match]; + if (elem_name === generic) { + match = elem_name; + break; } - total += lev; - done += 1; - } else { - return MAX_LEV_DISTANCE + 1; } } - return Math.ceil(total / done); + if (match === null) { + return MAX_LEV_DISTANCE + 1; + } + elems[match] -= 1; + if (elems[match] === 0) { + delete elems[match]; + } } + return 0; } return MAX_LEV_DISTANCE + 1; } /** - * This function checks if the object (`obj`) matches the given type (`val`) and its + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * + * @return {integer} - Returns a Levenshtein distance to the best match. + */ + function checkIfInGenerics(row, elem) { + var lev = MAX_LEV_DISTANCE + 1; + for (var x = 0, length = row[GENERICS_DATA].length; x < length && lev !== 0; ++x) { + lev = Math.min( + checkType(row[GENERICS_DATA][x], elem, true), + lev + ); + } + return lev; + } + + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its * generics (if any). * - * @param {Object} obj - * @param {string} val + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. * @param {boolean} literalSearch * * @return {integer} - Returns a Levenshtein distance to the best match. If there is * no match, returns `MAX_LEV_DISTANCE + 1`. */ - function checkType(obj, val, literalSearch) { - var lev_distance = MAX_LEV_DISTANCE + 1; - var tmp_lev = MAX_LEV_DISTANCE + 1; - var len, x, firstGeneric; - if (obj[NAME] === val.name) { - if (literalSearch) { - if (val.generics && val.generics.length !== 0) { - if (obj.length > GENERICS_DATA && - obj[GENERICS_DATA].length > 0) { - var elems = Object.create(null); - len = obj[GENERICS_DATA].length; - for (x = 0; x < len; ++x) { - if (!elems[obj[GENERICS_DATA][x][NAME]]) { - elems[obj[GENERICS_DATA][x][NAME]] = 0; - } - elems[obj[GENERICS_DATA][x][NAME]] += 1; - } + function checkType(row, elem, literalSearch) { + if (row[NAME].length === 0) { + // This is a pure "generic" search, no need to run other checks. + if (row.length > GENERICS_DATA) { + return checkIfInGenerics(row, elem); + } + return MAX_LEV_DISTANCE + 1; + } - len = val.generics.length; - for (x = 0; x < len; ++x) { - firstGeneric = val.generics[x]; - if (elems[firstGeneric]) { - elems[firstGeneric] -= 1; - } else { - // Something wasn't found and this is a literal search so - // abort and return a "failing" distance. - return MAX_LEV_DISTANCE + 1; - } - } - // Everything was found, success! + var lev = levenshtein(row[NAME], elem.name); + if (literalSearch) { + if (lev !== 0) { + // The name didn't match, let's try to check if the generics do. + if (elem.generics.length === 0) { + var checkGeneric = (row.length > GENERICS_DATA && + row[GENERICS_DATA].length > 0); + if (checkGeneric && row[GENERICS_DATA].findIndex(function(tmp_elem) { + return tmp_elem[NAME] === elem.name; + }) !== -1) { return 0; } - return MAX_LEV_DISTANCE + 1; } - return 0; - } else { - // If the type has generics but don't match, then it won't return at this point. - // Otherwise, `checkGenerics` will return 0 and it'll return. - if (obj.length > GENERICS_DATA && obj[GENERICS_DATA].length !== 0) { - tmp_lev = checkGenerics(obj, val); - if (tmp_lev <= MAX_LEV_DISTANCE) { - return tmp_lev; - } - } - } - } else if (literalSearch) { - var found = false; - if ((!val.generics || val.generics.length === 0) && - obj.length > GENERICS_DATA && obj[GENERICS_DATA].length > 0) { - found = obj[GENERICS_DATA].some( - function(gen) { - return gen[NAME] === val.name; - }); - } - return found ? 0 : MAX_LEV_DISTANCE + 1; - } - lev_distance = Math.min(levenshtein(obj[NAME], val.name), lev_distance); - if (lev_distance <= MAX_LEV_DISTANCE) { - // The generics didn't match but the name kinda did so we give it - // a levenshtein distance value that isn't *this* good so it goes - // into the search results but not too high. - lev_distance = Math.ceil((checkGenerics(obj, val) + lev_distance) / 2); - } - if (obj.length > GENERICS_DATA && obj[GENERICS_DATA].length > 0) { - // We can check if the type we're looking for is inside the generics! - var olength = obj[GENERICS_DATA].length; - for (x = 0; x < olength; ++x) { - tmp_lev = Math.min(levenshtein(obj[GENERICS_DATA][x][NAME], val.name), tmp_lev); + return MAX_LEV_DISTANCE + 1; + } else if (elem.generics.length > 0) { + return checkGenerics(row, elem, MAX_LEV_DISTANCE + 1); } - if (tmp_lev !== 0) { - // If we didn't find a good enough result, we go check inside the generics of - // the generics. - for (x = 0; x < olength && tmp_lev !== 0; ++x) { - tmp_lev = Math.min( - checkType(obj[GENERICS_DATA][x], val, literalSearch), - tmp_lev - ); + return 0; + } else if (row.length > GENERICS_DATA) { + if (elem.generics.length === 0) { + if (lev === 0) { + return 0; + } + // The name didn't match so we now check if the type we're looking for is inside + // the generics! + lev = checkIfInGenerics(row, elem); + // Now whatever happens, the returned distance is "less good" so we should mark + // it as such, and so we add 0.5 to the distance to make it "less good". + return lev + 0.5; + } else if (lev > MAX_LEV_DISTANCE) { + // So our item's name doesn't match at all and has generics. + // + // Maybe it's present in a sub generic? For example "f<A<B<C>>>()", if we're + // looking for "B<C>", we'll need to go down. + return checkIfInGenerics(row, elem); + } else { + // At this point, the name kinda match and we have generics to check, so + // let's go! + var tmp_lev = checkGenerics(row, elem, lev); + if (tmp_lev > MAX_LEV_DISTANCE) { + return MAX_LEV_DISTANCE + 1; } + // We compute the median value of both checks and return it. + return (tmp_lev + lev) / 2; } + } else if (elem.generics.length > 0) { + // In this case, we were expecting generics but there isn't so we simply reject this + // one. + return MAX_LEV_DISTANCE + 1; } - // Now whatever happens, the returned distance is "less good" so we should mark it - // as such, and so we add 1 to the distance to make it "less good". - return Math.min(lev_distance, tmp_lev) + 1; + // No generics on our query or on the target type so we can return without doing + // anything else. + return lev; } /** - * This function checks if the object (`obj`) has an argument with the given type (`val`). + * This function checks if the object (`row`) has an argument with the given type (`elem`). * - * @param {Object} obj - * @param {string} val - * @param {boolean} literalSearch + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. * @param {integer} typeFilter * * @return {integer} - Returns a Levenshtein distance to the best match. If there is no * match, returns `MAX_LEV_DISTANCE + 1`. */ - function findArg(obj, val, literalSearch, typeFilter) { - var lev_distance = MAX_LEV_DISTANCE + 1; + function findArg(row, elem, typeFilter) { + var lev = MAX_LEV_DISTANCE + 1; - if (obj && obj.type && obj.type[INPUTS_DATA] && obj.type[INPUTS_DATA].length > 0) { - var length = obj.type[INPUTS_DATA].length; + if (row && row.type && row.type[INPUTS_DATA] && row.type[INPUTS_DATA].length > 0) { + var length = row.type[INPUTS_DATA].length; for (var i = 0; i < length; i++) { - var tmp = obj.type[INPUTS_DATA][i]; + var tmp = row.type[INPUTS_DATA][i]; if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - tmp = checkType(tmp, val, literalSearch); - if (tmp === 0) { + lev = Math.min(lev, checkType(tmp, elem, parsedQuery.literalSearch)); + if (lev === 0) { return 0; - } else if (literalSearch) { - continue; } - lev_distance = Math.min(tmp, lev_distance); } } - return literalSearch ? MAX_LEV_DISTANCE + 1 : lev_distance; + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } - function checkReturned(obj, val, literalSearch, typeFilter) { - var lev_distance = MAX_LEV_DISTANCE + 1; + /** + * This function checks if the object (`row`) returns the given type (`elem`). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} typeFilter + * + * @return {integer} - Returns a Levenshtein distance to the best match. If there is no + * match, returns `MAX_LEV_DISTANCE + 1`. + */ + function checkReturned(row, elem, typeFilter) { + var lev = MAX_LEV_DISTANCE + 1; - if (obj && obj.type && obj.type.length > OUTPUT_DATA) { - var ret = obj.type[OUTPUT_DATA]; + if (row && row.type && row.type.length > OUTPUT_DATA) { + var ret = row.type[OUTPUT_DATA]; if (typeof ret[0] === "string") { ret = [ret]; } @@ -537,16 +1073,13 @@ window.initSearch = function(rawSearchIndex) { if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - tmp = checkType(tmp, val, literalSearch); - if (tmp === 0) { + lev = Math.min(lev, checkType(tmp, elem, parsedQuery.literalSearch)); + if (lev === 0) { return 0; - } else if (literalSearch) { - continue; } - lev_distance = Math.min(tmp, lev_distance); } } - return literalSearch ? MAX_LEV_DISTANCE + 1 : lev_distance; + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } function checkPath(contains, lastElem, ty) { @@ -621,13 +1154,14 @@ window.initSearch = function(rawSearchIndex) { } function handleAliases(ret, query, filterCrates) { + var lowerQuery = query.toLowerCase(); // We separate aliases and crate aliases because we want to have current crate // aliases to be before the others in the displayed results. var aliases = []; var crateAliases = []; if (filterCrates !== null) { - if (ALIASES[filterCrates] && ALIASES[filterCrates][query.search]) { - var query_aliases = ALIASES[filterCrates][query.search]; + if (ALIASES[filterCrates] && ALIASES[filterCrates][lowerQuery]) { + var query_aliases = ALIASES[filterCrates][lowerQuery]; var len = query_aliases.length; for (var i = 0; i < len; ++i) { aliases.push(createAliasFromItem(searchIndex[query_aliases[i]])); @@ -635,9 +1169,9 @@ window.initSearch = function(rawSearchIndex) { } } else { Object.keys(ALIASES).forEach(function(crate) { - if (ALIASES[crate][query.search]) { + if (ALIASES[crate][lowerQuery]) { var pushTo = crate === window.currentCrate ? crateAliases : aliases; - var query_aliases = ALIASES[crate][query.search]; + var query_aliases = ALIASES[crate][lowerQuery]; var len = query_aliases.length; for (var i = 0; i < len; ++i) { pushTo.push(createAliasFromItem(searchIndex[query_aliases[i]])); @@ -658,7 +1192,7 @@ window.initSearch = function(rawSearchIndex) { aliases.sort(sortFunc); var pushFunc = function(alias) { - alias.alias = query.raw; + alias.alias = query; var res = buildHrefAndPath(alias); alias.displayPath = pathSplitter(res[0]); alias.fullPath = alias.displayPath + alias.name; @@ -674,208 +1208,237 @@ window.initSearch = function(rawSearchIndex) { } /** - * This function adds the given result into the provided `res` map if it matches the + * This function adds the given result into the provided `results` map if it matches the * following condition: * - * * If it is a "literal search" (`isExact`), then `lev` must be 0. + * * If it is a "literal search" (`parsedQuery.literalSearch`), then `lev` must be 0. * * If it is not a "literal search", `lev` must be <= `MAX_LEV_DISTANCE`. * - * The `res` map contains information which will be used to sort the search results: + * The `results` map contains information which will be used to sort the search results: * - * * `fullId` is a `string`` used as the key of the object we use for the `res` map. + * * `fullId` is a `string`` used as the key of the object we use for the `results` map. * * `id` is the index in both `searchWords` and `searchIndex` arrays for this element. * * `index` is an `integer`` used to sort by the position of the word in the item's name. * * `lev` is the main metric used to sort the search results. * - * @param {boolean} isExact - * @param {Object} res + * @param {Results} results * @param {string} fullId * @param {integer} id * @param {integer} index * @param {integer} lev */ - function addIntoResults(isExact, res, fullId, id, index, lev) { - if (lev === 0 || (!isExact && lev <= MAX_LEV_DISTANCE)) { - if (res[fullId] !== undefined) { - var result = res[fullId]; + function addIntoResults(results, fullId, id, index, lev) { + if (lev === 0 || (!parsedQuery.literalSearch && lev <= MAX_LEV_DISTANCE)) { + if (results[fullId] !== undefined) { + var result = results[fullId]; if (result.dontValidate || result.lev <= lev) { return; } } - res[fullId] = { + results[fullId] = { id: id, index: index, - dontValidate: isExact, + dontValidate: parsedQuery.literalSearch, lev: lev, }; } } - // quoted values mean literal search - var nSearchWords = searchWords.length; - var i, it; - var ty; - var fullId; - var returned; - var in_args; - var len; - if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && - val.charAt(val.length - 1) === val.charAt(0)) - { - val = extractGenerics(val.substr(1, val.length - 2)); - for (i = 0; i < nSearchWords; ++i) { - if (filterCrates !== null && searchIndex[i].crate !== filterCrates) { - continue; + /** + * This function is called in case the query is only one element (with or without generics). + * This element will be compared to arguments' and returned values' items and also to items. + * + * Other important thing to note: since there is only one element, we use levenshtein + * distance for name comparisons. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {QueryElement} elem - The element from the parsed query. + * @param {Results} results_others - Unqualified results (not in arguments nor in + * returned values). + * @param {Results} results_in_args - Matching arguments results. + * @param {Results} results_returned - Matching returned arguments results. + */ + function handleSingleArg( + row, + pos, + elem, + results_others, + results_in_args, + results_returned + ) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } + var lev, lev_add = 0, index = -1; + var fullId = row.id; + + var in_args = findArg(row, elem, parsedQuery.typeFilter); + var returned = checkReturned(row, elem, parsedQuery.typeFilter); + + addIntoResults(results_in_args, fullId, pos, index, in_args); + addIntoResults(results_returned, fullId, pos, index, returned); + + if (!typePassesFilter(parsedQuery.typeFilter, row.ty)) { + return; + } + var searchWord = searchWords[pos]; + + if (parsedQuery.literalSearch) { + if (searchWord === elem.name) { + addIntoResults(results_others, fullId, pos, -1, 0); } - in_args = findArg(searchIndex[i], val, true, typeFilter); - returned = checkReturned(searchIndex[i], val, true, typeFilter); - ty = searchIndex[i]; - fullId = ty.id; - - if (searchWords[i] === val.name - && typePassesFilter(typeFilter, searchIndex[i].ty)) { - addIntoResults(true, results, fullId, i, -1, 0); + return; + } + + // No need to check anything else if it's a "pure" generics search. + if (elem.name.length === 0) { + if (row.type !== null) { + lev = checkGenerics(row.type, elem, MAX_LEV_DISTANCE + 1); + addIntoResults(results_others, fullId, pos, index, lev); } - addIntoResults(true, results_in_args, fullId, i, -1, in_args); - addIntoResults(true, results_returned, fullId, i, -1, returned); - } - query.inputs = [val]; - query.output = val; - query.search = val; - // searching by type - } else if (val.search("->") > -1) { - var trimmer = function(s) { return s.trim(); }; - var parts = val.split("->").map(trimmer); - var input = parts[0]; - // sort inputs so that order does not matter - var inputs = input.split(",").map(trimmer).sort(); - for (i = 0, len = inputs.length; i < len; ++i) { - inputs[i] = extractGenerics(inputs[i]); - } - var output = extractGenerics(parts[1]); - - for (i = 0; i < nSearchWords; ++i) { - if (filterCrates !== null && searchIndex[i].crate !== filterCrates) { - continue; + return; + } + + if (elem.fullPath.length > 1) { + lev = checkPath(elem.pathWithoutLast, elem.pathLast, row); + if (lev > MAX_LEV_DISTANCE || (parsedQuery.literalSearch && lev !== 0)) { + return; + } else if (lev > 0) { + lev_add = lev / 10; } - var type = searchIndex[i].type; - ty = searchIndex[i]; - if (!type) { - continue; + } + + if (searchWord.indexOf(elem.pathLast) > -1 || + row.normalizedName.indexOf(elem.pathLast) > -1) + { + // filter type: ... queries + if (!results_others[fullId] !== undefined) { + index = row.normalizedName.indexOf(elem.pathLast); } - fullId = ty.id; + } + lev = levenshtein(searchWord, elem.pathLast); + lev += lev_add; + if (lev > 0 && elem.pathLast.length > 2 && searchWord.indexOf(elem.pathLast) > -1) + { + if (elem.pathLast.length < 6) { + lev = 1; + } else { + lev = 0; + } + } + if (lev > MAX_LEV_DISTANCE) { + return; + } else if (index !== -1 && elem.fullPath.length < 2) { + lev -= 1; + } + if (lev < 0) { + lev = 0; + } + addIntoResults(results_others, fullId, pos, index, lev); + } - returned = checkReturned(ty, output, true, NO_TYPE_FILTER); - if (output.name === "*" || returned === 0) { - in_args = false; - var is_module = false; + /** + * This function is called in case the query has more than one element. In this case, it'll + * try to match the items which validates all the elements. For `aa -> bb` will look for + * functions which have a parameter `aa` and has `bb` in its returned values. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {Object} results + */ + function handleArgs(row, pos, results) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } - if (input === "*") { - is_module = true; + var totalLev = 0; + var nbLev = 0; + var lev; + + // If the result is too "bad", we return false and it ends this search. + function checkArgs(elems, callback) { + for (var i = 0, len = elems.length; i < len; ++i) { + var elem = elems[i]; + // There is more than one parameter to the query so all checks should be "exact" + lev = callback(row, elem, NO_TYPE_FILTER); + if (lev <= 1) { + nbLev += 1; + totalLev += lev; } else { - var firstNonZeroDistance = 0; - for (it = 0, len = inputs.length; it < len; it++) { - var distance = checkType(type, inputs[it], true); - if (distance > 0) { - firstNonZeroDistance = distance; - break; - } - } - in_args = firstNonZeroDistance; - } - addIntoResults(true, results_in_args, fullId, i, -1, in_args); - addIntoResults(true, results_returned, fullId, i, -1, returned); - if (is_module) { - addIntoResults(true, results, fullId, i, -1, 0); + return false; } } + return true; + } + if (!checkArgs(parsedQuery.elems, findArg)) { + return; + } + if (!checkArgs(parsedQuery.returned, checkReturned)) { + return; } - query.inputs = inputs.map(function(input) { - return input.name; - }); - query.output = output.name; - } else { - query.inputs = [val]; - query.output = val; - query.search = val; - // gather matching search results up to a certain maximum - val = val.replace(/_/g, ""); - - var valGenerics = extractGenerics(val); - - var paths = valLower.split("::"); - removeEmptyStringsFromArray(paths); - val = paths[paths.length - 1]; - var contains = paths.slice(0, paths.length > 1 ? paths.length - 1 : 1); - - var lev, j; - for (j = 0; j < nSearchWords; ++j) { - ty = searchIndex[j]; - if (!ty || (filterCrates !== null && ty.crate !== filterCrates)) { - continue; - } - var lev_add = 0; - if (paths.length > 1) { - lev = checkPath(contains, paths[paths.length - 1], ty); - if (lev > MAX_LEV_DISTANCE) { - continue; - } else if (lev > 0) { - lev_add = lev / 10; - } - } - returned = MAX_LEV_DISTANCE + 1; - in_args = MAX_LEV_DISTANCE + 1; - var index = -1; - // we want lev results to go lower than others - lev = MAX_LEV_DISTANCE + 1; - fullId = ty.id; + if (nbLev === 0) { + return; + } + lev = Math.round(totalLev / nbLev); + addIntoResults(results, row.id, pos, 0, lev); + } - if (searchWords[j].indexOf(split[i]) > -1 || - searchWords[j].indexOf(val) > -1 || - ty.normalizedName.indexOf(val) > -1) - { - // filter type: ... queries - if (typePassesFilter(typeFilter, ty.ty) && results[fullId] === undefined) { - index = ty.normalizedName.indexOf(val); + function innerRunQuery() { + var elem, i, nSearchWords, in_returned, row; + + if (parsedQuery.foundElems === 1) { + if (parsedQuery.elems.length === 1) { + elem = parsedQuery.elems[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + // It means we want to check for this element everywhere (in names, args and + // returned). + handleSingleArg( + searchIndex[i], + i, + elem, + results_others, + results_in_args, + results_returned + ); } - } - if ((lev = levenshtein(searchWords[j], val)) <= MAX_LEV_DISTANCE) { - if (typePassesFilter(typeFilter, ty.ty)) { - lev += 1; - } else { - lev = MAX_LEV_DISTANCE + 1; + } else if (parsedQuery.returned.length === 1) { + // We received one returned argument to check, so looking into returned values. + elem = parsedQuery.returned[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + row = searchIndex[i]; + in_returned = checkReturned(row, elem, parsedQuery.typeFilter); + addIntoResults(results_returned, row.id, i, -1, in_returned); } } - in_args = findArg(ty, valGenerics, false, typeFilter); - returned = checkReturned(ty, valGenerics, false, typeFilter); - - lev += lev_add; - if (lev > 0 && val.length > 3 && searchWords[j].indexOf(val) > -1) { - if (val.length < 6) { - lev -= 1; - } else { - lev = 0; - } + } else if (parsedQuery.foundElems > 0) { + var container = results_others; + // In the special case where only a "returned" information is available, we want to + // put the information into the "results_returned" dict. + if (parsedQuery.returned.length !== 0 && parsedQuery.elems.length === 0) { + container = results_returned; } - addIntoResults(false, results_in_args, fullId, j, index, in_args); - addIntoResults(false, results_returned, fullId, j, index, returned); - if (typePassesFilter(typeFilter, ty.ty) && - (index !== -1 || lev <= MAX_LEV_DISTANCE)) { - if (index !== -1 && paths.length < 2) { - lev = 0; - } - addIntoResults(false, results, fullId, j, index, lev); + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + handleArgs(searchIndex[i], i, container); } } } - var ret = { - "in_args": sortResults(results_in_args, true), - "returned": sortResults(results_returned, true), - "others": sortResults(results, false), - }; - handleAliases(ret, query, filterCrates); + if (parsedQuery.error === null) { + innerRunQuery(); + } + + var ret = createQueryResults( + sortResults(results_in_args, true), + sortResults(results_returned, true), + sortResults(results_others, false), + parsedQuery); + handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates); + if (parsedQuery.error !== null && ret.others.length !== 0) { + // It means some doc aliases were found so let's "remove" the error! + ret.query.error = null; + } return ret; } @@ -892,9 +1455,13 @@ window.initSearch = function(rawSearchIndex) { * @param {string} path - The path of the result * @param {string} keys - The keys to be used (["file", "open"]) * @param {Object} parent - The parent of the result + * * @return {boolean} - Whether the result is valid or not */ function validateResult(name, path, keys, parent) { + if (!keys || !keys.length) { + return true; + } for (var i = 0, len = keys.length; i < len; ++i) { // each check is for validation so we negate the conditions and invalidate if (!( @@ -913,30 +1480,6 @@ window.initSearch = function(rawSearchIndex) { return true; } - /** - * Parse a string into a query object. - * - * @param {string} raw - The text that the user typed. - * @returns {ParsedQuery} - */ - function getQuery(raw) { - var matches, type = "", query; - query = raw; - - matches = query.match(/^(fn|mod|struct|enum|trait|type|const|macro)\s*:\s*/i); - if (matches) { - type = matches[1].replace(/^const$/, "constant"); - query = query.substring(matches[0].length); - } - - return { - raw: raw, - query: query, - type: type, - id: query + type - }; - } - function nextTab(direction) { var next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; searchState.focusedByTab[searchState.currentTab] = document.activeElement; @@ -1088,11 +1631,11 @@ window.initSearch = function(rawSearchIndex) { link.appendChild(wrapper); output.appendChild(link); }); - } else { + } else if (query.error === null) { output.className = "search-failed" + extraClass; output.innerHTML = "No results :(<br/>" + "Try on <a href=\"https://duckduckgo.com/?q=" + - encodeURIComponent("rust " + query.query) + + encodeURIComponent("rust " + query.userQuery) + "\">DuckDuckGo</a>?<br/><br/>" + "Or try looking in one of these:<ul><li>The <a " + "href=\"https://doc.rust-lang.org/reference/index.html\">Rust Reference</a> " + @@ -1115,6 +1658,11 @@ window.initSearch = function(rawSearchIndex) { return "<button>" + text + " <div class=\"count\">(" + nbElems + ")</div></button>"; } + /** + * @param {ResultsTable} results + * @param {boolean} go_to_first + * @param {string} filterCrates + */ function showResults(results, go_to_first, filterCrates) { var search = searchState.outputElement(); if (go_to_first || (results.others.length === 1 @@ -1132,13 +1680,15 @@ window.initSearch = function(rawSearchIndex) { elem.click(); return; } - var query = getQuery(searchState.input.value); + if (results.query === undefined) { + results.query = parseQuery(searchState.input.value); + } - currentResults = query.id; + currentResults = results.query.userQuery; - var ret_others = addTab(results.others, query, true); - var ret_in_args = addTab(results.in_args, query, false); - var ret_returned = addTab(results.returned, query, false); + var ret_others = addTab(results.others, results.query, true); + var ret_in_args = addTab(results.in_args, results.query, false); + var ret_returned = addTab(results.returned, results.query, false); // Navigate to the relevant tab if the current tab is empty, like in case users search // for "-> String". If they had selected another tab previously, they have to click on @@ -1164,11 +1714,19 @@ window.initSearch = function(rawSearchIndex) { } crates += `</select>`; } - var output = `<div id="search-settings"> - <h1 class="search-results-title">Results for ${escape(query.query)} ` + - (query.type ? " (type: " + escape(query.type) + ")" : "") + "</h1>" + - crates + - `</div><div id="titles">` + + + var typeFilter = ""; + if (results.query.typeFilter !== NO_TYPE_FILTER) { + typeFilter = " (type: " + escape(itemTypes[results.query.typeFilter]) + ")"; + } + + var output = `<div id="search-settings">` + + `<h1 class="search-results-title">Results for ${escape(results.query.userQuery)}` + + `${typeFilter}</h1> in ${crates} </div>`; + if (results.query.error !== null) { + output += `<h3>Query parser error: "${results.query.error}".</h3>`; + } + output += `<div id="titles">` + makeTabHeader(0, "In Names", ret_others[1]) + makeTabHeader(1, "In Parameters", ret_in_args[1]) + makeTabHeader(2, "In Return Types", ret_returned[1]) + @@ -1196,28 +1754,6 @@ window.initSearch = function(rawSearchIndex) { printTab(currentTab); } - function execSearch(query, searchWords, filterCrates) { - query = query.raw.trim(); - var results = { - "in_args": [], - "returned": [], - "others": [], - }; - - if (query.length !== 0) { - var tmp = execQuery(getQuery(query), searchWords, filterCrates); - - results.in_args.push(tmp.in_args); - results.returned.push(tmp.returned); - results.others.push(tmp.others); - } - return { - "in_args": results.in_args[0], - "returned": results.returned[0], - "others": results.others[0], - }; - } - /** * Perform a search based on the current state of the search input element * and display the results. @@ -1226,17 +1762,14 @@ window.initSearch = function(rawSearchIndex) { */ function search(e, forced) { var params = searchState.getQueryStringParams(); - var query = getQuery(searchState.input.value.trim()); + var query = parseQuery(searchState.input.value.trim()); if (e) { e.preventDefault(); } - if (query.query.length === 0) { - return; - } - if (!forced && query.id === currentResults) { - if (query.query.length > 0) { + if (!forced && query.userQuery === currentResults) { + if (query.userQuery.length > 0) { putBackSearch(); } return; @@ -1251,13 +1784,12 @@ window.initSearch = function(rawSearchIndex) { } // Update document title to maintain a meaningful browser history - searchState.title = "Results for " + query.query + " - Rust"; + searchState.title = "Results for " + query.original + " - Rust"; // Because searching is incremental by character, only the most // recent search query is added to the browser history. if (searchState.browserSupportsHistoryApi()) { - var newURL = buildUrl(query.raw, filterCrates); - + var newURL = buildUrl(query.original, filterCrates); if (!history.state && !params.search) { history.pushState(null, "", newURL); } else { @@ -1265,8 +1797,10 @@ window.initSearch = function(rawSearchIndex) { } } - showResults(execSearch(query, searchWords, filterCrates), - params["go_to_first"], filterCrates); + showResults( + execQuery(query, searchWords, filterCrates), + params.go_to_first, + filterCrates); } function buildIndex(rawSearchIndex) { diff --git a/src/test/rustdoc-js-std/filter-crate.js b/src/test/rustdoc-js-std/filter-crate.js index 2e0330c4497..b47a1fefa41 100644 --- a/src/test/rustdoc-js-std/filter-crate.js +++ b/src/test/rustdoc-js-std/filter-crate.js @@ -1,6 +1,6 @@ // exact-check -const QUERY = 'hashmap'; +const QUERY = '"hashmap"'; const FILTER_CRATE = 'core'; const EXPECTED = { diff --git a/src/test/rustdoc-js-std/parser-errors.js b/src/test/rustdoc-js-std/parser-errors.js new file mode 100644 index 00000000000..779ab867c12 --- /dev/null +++ b/src/test/rustdoc-js-std/parser-errors.js @@ -0,0 +1,365 @@ +const QUERY = [ + '<P>', + '-> <P>', + 'a<"P">', + '"P" "P"', + 'P "P"', + '"p" p', + '"const": p', + "a<:a>", + "a<::a>", + "((a))", + "(p -> p", + "::a::b", + "a::::b", + "a::b::", + ":a", + "a b:", + "a (b:", + "_:", + "a-bb", + "a>bb", + "ab'", + "a->", + '"p" <a>', + '"p" a<a>', + "a,<", + "aaaaa<>b", + "fn:aaaaa<>b", + "->a<>b", + "a<->", + "a:: a", + "a ::a", + "a<a>:", + "a<>:", + "a,:", + " a<> :", + "mod : :", +]; + +const PARSED = [ + { + elems: [], + foundElems: 0, + original: "<P>", + returned: [], + typeFilter: -1, + userQuery: "<p>", + error: "Found generics without a path", + }, + { + elems: [], + foundElems: 0, + original: "-> <P>", + returned: [], + typeFilter: -1, + userQuery: "-> <p>", + error: "Found generics without a path", + }, + { + elems: [], + foundElems: 0, + original: "a<\"P\">", + returned: [], + typeFilter: -1, + userQuery: "a<\"p\">", + error: "`\"` cannot be used in generics", + }, + { + elems: [], + foundElems: 0, + original: "\"P\" \"P\"", + returned: [], + typeFilter: -1, + userQuery: "\"p\" \"p\"", + error: "Cannot have more than one literal search element", + }, + { + elems: [], + foundElems: 0, + original: "P \"P\"", + returned: [], + typeFilter: -1, + userQuery: "p \"p\"", + error: "Cannot use literal search when there is more than one element", + }, + { + elems: [], + foundElems: 0, + original: "\"p\" p", + returned: [], + typeFilter: -1, + userQuery: "\"p\" p", + error: "You cannot have more than one element if you use quotes", + }, + { + elems: [], + foundElems: 0, + original: "\"const\": p", + returned: [], + typeFilter: -1, + userQuery: "\"const\": p", + error: "You cannot use quotes on type filter", + }, + { + elems: [], + foundElems: 0, + original: "a<:a>", + returned: [], + typeFilter: -1, + userQuery: "a<:a>", + error: "Unexpected `:` after `<`", + }, + { + elems: [], + foundElems: 0, + original: "a<::a>", + returned: [], + typeFilter: -1, + userQuery: "a<::a>", + error: "Unexpected `::`: paths cannot start with `::`", + }, + { + elems: [], + foundElems: 0, + original: "((a))", + returned: [], + typeFilter: -1, + userQuery: "((a))", + error: "Unexpected `(`", + }, + { + elems: [], + foundElems: 0, + original: "(p -> p", + returned: [], + typeFilter: -1, + userQuery: "(p -> p", + error: "Unexpected `(`", + }, + { + elems: [], + foundElems: 0, + original: "::a::b", + returned: [], + typeFilter: -1, + userQuery: "::a::b", + error: "Paths cannot start with `::`", + }, + { + elems: [], + foundElems: 0, + original: "a::::b", + returned: [], + typeFilter: -1, + userQuery: "a::::b", + error: "Unexpected `::::`", + }, + { + elems: [], + foundElems: 0, + original: "a::b::", + returned: [], + typeFilter: -1, + userQuery: "a::b::", + error: "Paths cannot end with `::`", + }, + { + elems: [], + foundElems: 0, + original: ":a", + returned: [], + typeFilter: -1, + userQuery: ":a", + error: "Expected type filter before `:`", + }, + { + elems: [], + foundElems: 0, + original: "a b:", + returned: [], + typeFilter: -1, + userQuery: "a b:", + error: "Unexpected `:`", + }, + { + elems: [], + foundElems: 0, + original: "a (b:", + returned: [], + typeFilter: -1, + userQuery: "a (b:", + error: "Unexpected `(`", + }, + { + elems: [], + foundElems: 0, + original: "_:", + returned: [], + typeFilter: -1, + userQuery: "_:", + error: "Unknown type filter `_`", + }, + { + elems: [], + foundElems: 0, + original: "a-bb", + returned: [], + typeFilter: -1, + userQuery: "a-bb", + error: "Unexpected `-` (did you mean `->`?)", + }, + { + elems: [], + foundElems: 0, + original: "a>bb", + returned: [], + typeFilter: -1, + userQuery: "a>bb", + error: "Unexpected `>` (did you mean `->`?)", + }, + { + elems: [], + foundElems: 0, + original: "ab'", + returned: [], + typeFilter: -1, + userQuery: "ab'", + error: "Unexpected `'`", + }, + { + elems: [], + foundElems: 0, + original: "a->", + returned: [], + typeFilter: -1, + userQuery: "a->", + error: "Expected at least one item after `->`", + }, + { + elems: [], + foundElems: 0, + original: '"p" <a>', + returned: [], + typeFilter: -1, + userQuery: '"p" <a>', + error: "Found generics without a path", + }, + { + elems: [], + foundElems: 0, + original: '"p" a<a>', + returned: [], + typeFilter: -1, + userQuery: '"p" a<a>', + error: "You cannot have more than one element if you use quotes", + }, + { + elems: [], + foundElems: 0, + original: 'a,<', + returned: [], + typeFilter: -1, + userQuery: 'a,<', + error: 'Found generics without a path', + }, + { + elems: [], + foundElems: 0, + original: 'aaaaa<>b', + returned: [], + typeFilter: -1, + userQuery: 'aaaaa<>b', + error: 'Expected `,`, ` `, `:` or `->`, found `b`', + }, + { + elems: [], + foundElems: 0, + original: 'fn:aaaaa<>b', + returned: [], + typeFilter: -1, + userQuery: 'fn:aaaaa<>b', + error: 'Expected `,`, ` ` or `->`, found `b`', + }, + { + elems: [], + foundElems: 0, + original: '->a<>b', + returned: [], + typeFilter: -1, + userQuery: '->a<>b', + error: 'Expected `,` or ` `, found `b`', + }, + { + elems: [], + foundElems: 0, + original: 'a<->', + returned: [], + typeFilter: -1, + userQuery: 'a<->', + error: 'Unexpected `-` after `<`', + }, + { + elems: [], + foundElems: 0, + original: 'a:: a', + returned: [], + typeFilter: -1, + userQuery: 'a:: a', + error: 'Paths cannot end with `::`', + }, + { + elems: [], + foundElems: 0, + original: 'a ::a', + returned: [], + typeFilter: -1, + userQuery: 'a ::a', + error: 'Paths cannot start with `::`', + }, + { + elems: [], + foundElems: 0, + original: "a<a>:", + returned: [], + typeFilter: -1, + userQuery: "a<a>:", + error: 'Unexpected `:`', + }, + { + elems: [], + foundElems: 0, + original: "a<>:", + returned: [], + typeFilter: -1, + userQuery: "a<>:", + error: 'Unexpected `<` in type filter', + }, + { + elems: [], + foundElems: 0, + original: "a,:", + returned: [], + typeFilter: -1, + userQuery: "a,:", + error: 'Unexpected `,` in type filter', + }, + { + elems: [], + foundElems: 0, + original: "a<> :", + returned: [], + typeFilter: -1, + userQuery: "a<> :", + error: 'Unexpected `<` in type filter', + }, + { + elems: [], + foundElems: 0, + original: "mod : :", + returned: [], + typeFilter: -1, + userQuery: "mod : :", + error: 'Unexpected `:`', + }, +]; diff --git a/src/test/rustdoc-js-std/parser-filter.js b/src/test/rustdoc-js-std/parser-filter.js new file mode 100644 index 00000000000..e5a87a415ac --- /dev/null +++ b/src/test/rustdoc-js-std/parser-filter.js @@ -0,0 +1,43 @@ +const QUERY = ['fn:foo', 'enum : foo', 'macro<f>:foo']; + +const PARSED = [ + { + elems: [{ + name: "foo", + fullPath: ["foo"], + pathWithoutLast: [], + pathLast: "foo", + generics: [], + }], + foundElems: 1, + original: "fn:foo", + returned: [], + typeFilter: 5, + userQuery: "fn:foo", + error: null, + }, + { + elems: [{ + name: "foo", + fullPath: ["foo"], + pathWithoutLast: [], + pathLast: "foo", + generics: [], + }], + foundElems: 1, + original: "enum : foo", + returned: [], + typeFilter: 4, + userQuery: "enum : foo", + error: null, + }, + { + elems: [], + foundElems: 0, + original: "macro<f>:foo", + returned: [], + typeFilter: -1, + userQuery: "macro<f>:foo", + error: "Unexpected `:`", + }, +]; diff --git a/src/test/rustdoc-js-std/parser-generics.js b/src/test/rustdoc-js-std/parser-generics.js new file mode 100644 index 00000000000..0cf7f5019aa --- /dev/null +++ b/src/test/rustdoc-js-std/parser-generics.js @@ -0,0 +1,62 @@ +const QUERY = ['A<B<C<D>, E>', 'p<> u8', '"p"<a>']; + +const PARSED = [ + { + elems: [], + foundElems: 0, + original: 'A<B<C<D>, E>', + returned: [], + typeFilter: -1, + userQuery: 'a<b<c<d>, e>', + error: 'Unexpected `<` after `<`', + }, + { + elems: [ + { + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }, + { + name: "u8", + fullPath: ["u8"], + pathWithoutLast: [], + pathLast: "u8", + generics: [], + }, + ], + foundElems: 2, + original: "p<> u8", + returned: [], + typeFilter: -1, + userQuery: "p<> u8", + error: null, + }, + { + elems: [ + { + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }, + ], + }, + ], + foundElems: 1, + original: '"p"<a>', + returned: [], + typeFilter: -1, + userQuery: '"p"<a>', + error: null, + }, +]; diff --git a/src/test/rustdoc-js-std/parser-literal.js b/src/test/rustdoc-js-std/parser-literal.js new file mode 100644 index 00000000000..87b3baff1e2 --- /dev/null +++ b/src/test/rustdoc-js-std/parser-literal.js @@ -0,0 +1,27 @@ +const QUERY = ['R<P>']; + +const PARSED = [ + { + elems: [{ + name: "r", + fullPath: ["r"], + pathWithoutLast: [], + pathLast: "r", + generics: [ + { + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }, + ], + }], + foundElems: 1, + original: "R<P>", + returned: [], + typeFilter: -1, + userQuery: "r<p>", + error: null, + } +]; diff --git a/src/test/rustdoc-js-std/parser-paths.js b/src/test/rustdoc-js-std/parser-paths.js new file mode 100644 index 00000000000..9f823f9336a --- /dev/null +++ b/src/test/rustdoc-js-std/parser-paths.js @@ -0,0 +1,90 @@ +const QUERY = ['A::B', 'A::B,C', 'A::B<f>,C', 'mod::a']; + +const PARSED = [ + { + elems: [{ + name: "a::b", + fullPath: ["a", "b"], + pathWithoutLast: ["a"], + pathLast: "b", + generics: [], + }], + foundElems: 1, + original: "A::B", + returned: [], + typeFilter: -1, + userQuery: "a::b", + error: null, + }, + { + elems: [ + { + name: "a::b", + fullPath: ["a", "b"], + pathWithoutLast: ["a"], + pathLast: "b", + generics: [], + }, + { + name: "c", + fullPath: ["c"], + pathWithoutLast: [], + pathLast: "c", + generics: [], + }, + ], + foundElems: 2, + original: 'A::B,C', + returned: [], + typeFilter: -1, + userQuery: 'a::b,c', + error: null, + }, + { + elems: [ + { + name: "a::b", + fullPath: ["a", "b"], + pathWithoutLast: ["a"], + pathLast: "b", + generics: [ + { + name: "f", + fullPath: ["f"], + pathWithoutLast: [], + pathLast: "f", + generics: [], + }, + ], + }, + { + name: "c", + fullPath: ["c"], + pathWithoutLast: [], + pathLast: "c", + generics: [], + }, + ], + foundElems: 2, + original: 'A::B<f>,C', + returned: [], + typeFilter: -1, + userQuery: 'a::b<f>,c', + error: null, + }, + { + elems: [{ + name: "mod::a", + fullPath: ["mod", "a"], + pathWithoutLast: ["mod"], + pathLast: "a", + generics: [], + }], + foundElems: 1, + original: "mod::a", + returned: [], + typeFilter: -1, + userQuery: "mod::a", + error: null, + }, +]; diff --git a/src/test/rustdoc-js-std/parser-quote.js b/src/test/rustdoc-js-std/parser-quote.js new file mode 100644 index 00000000000..1e16c90de5e --- /dev/null +++ b/src/test/rustdoc-js-std/parser-quote.js @@ -0,0 +1,87 @@ +const QUERY = [ + '-> "p"', + '"p",', + '"p" -> a', + '"a" -> "p"', + '->"-"', + '"a', + '""', +]; + +const PARSED = [ + { + elems: [], + foundElems: 1, + original: '-> "p"', + returned: [{ + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }], + typeFilter: -1, + userQuery: '-> "p"', + error: null, + }, + { + elems: [{ + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }], + foundElems: 1, + original: '"p",', + returned: [], + typeFilter: -1, + userQuery: '"p",', + error: null, + }, + { + elems: [], + foundElems: 0, + original: '"p" -> a', + returned: [], + typeFilter: -1, + userQuery: '"p" -> a', + error: "You cannot have more than one element if you use quotes", + }, + { + elems: [], + foundElems: 0, + original: '"a" -> "p"', + returned: [], + typeFilter: -1, + userQuery: '"a" -> "p"', + error: "Cannot have more than one literal search element", + }, + { + elems: [], + foundElems: 0, + original: '->"-"', + returned: [], + typeFilter: -1, + userQuery: '->"-"', + error: 'Unexpected `-` in a string element', + }, + { + elems: [], + foundElems: 0, + original: '"a', + returned: [], + typeFilter: -1, + userQuery: '"a', + error: 'Unclosed `"`', + }, + { + elems: [], + foundElems: 0, + original: '""', + returned: [], + typeFilter: -1, + userQuery: '""', + error: 'Cannot have empty string element', + }, +]; diff --git a/src/test/rustdoc-js-std/parser-returned.js b/src/test/rustdoc-js-std/parser-returned.js new file mode 100644 index 00000000000..b45466aa940 --- /dev/null +++ b/src/test/rustdoc-js-std/parser-returned.js @@ -0,0 +1,78 @@ +const QUERY = ['-> F<P>', '-> P', '->,a', 'aaaaa->a']; + +const PARSED = [ + { + elems: [], + foundElems: 1, + original: "-> F<P>", + returned: [{ + name: "f", + fullPath: ["f"], + pathWithoutLast: [], + pathLast: "f", + generics: [ + { + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }, + ], + }], + typeFilter: -1, + userQuery: "-> f<p>", + error: null, + }, + { + elems: [], + foundElems: 1, + original: "-> P", + returned: [{ + name: "p", + fullPath: ["p"], + pathWithoutLast: [], + pathLast: "p", + generics: [], + }], + typeFilter: -1, + userQuery: "-> p", + error: null, + }, + { + elems: [], + foundElems: 1, + original: "->,a", + returned: [{ + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }], + typeFilter: -1, + userQuery: "->,a", + error: null, + }, + { + elems: [{ + name: "aaaaa", + fullPath: ["aaaaa"], + pathWithoutLast: [], + pathLast: "aaaaa", + generics: [], + }], + foundElems: 2, + original: "aaaaa->a", + returned: [{ + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }], + typeFilter: -1, + userQuery: "aaaaa->a", + error: null, + }, +]; diff --git a/src/test/rustdoc-js-std/parser-separators.js b/src/test/rustdoc-js-std/parser-separators.js new file mode 100644 index 00000000000..5b7abdfa8d6 --- /dev/null +++ b/src/test/rustdoc-js-std/parser-separators.js @@ -0,0 +1,206 @@ +// ignore-tidy-tab + +const QUERY = [ + 'aaaaaa b', + 'a b', + 'a,b', + 'a\tb', + 'a<b c>', + 'a<b,c>', + 'a<b\tc>', +]; + +const PARSED = [ + { + elems: [ + { + name: 'aaaaaa', + fullPath: ['aaaaaa'], + pathWithoutLast: [], + pathLast: 'aaaaaa', + generics: [], + }, + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + ], + foundElems: 2, + original: "aaaaaa b", + returned: [], + typeFilter: -1, + userQuery: "aaaaaa b", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [], + }, + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + ], + foundElems: 2, + original: "a b", + returned: [], + typeFilter: -1, + userQuery: "a b", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [], + }, + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + ], + foundElems: 2, + original: "a,b", + returned: [], + typeFilter: -1, + userQuery: "a,b", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [], + }, + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + ], + foundElems: 2, + original: "a\tb", + returned: [], + typeFilter: -1, + userQuery: "a\tb", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [ + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + { + name: 'c', + fullPath: ['c'], + pathWithoutLast: [], + pathLast: 'c', + generics: [], + }, + ], + }, + ], + foundElems: 1, + original: "a<b c>", + returned: [], + typeFilter: -1, + userQuery: "a<b c>", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [ + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + { + name: 'c', + fullPath: ['c'], + pathWithoutLast: [], + pathLast: 'c', + generics: [], + }, + ], + }, + ], + foundElems: 1, + original: "a<b,c>", + returned: [], + typeFilter: -1, + userQuery: "a<b,c>", + error: null, + }, + { + elems: [ + { + name: 'a', + fullPath: ['a'], + pathWithoutLast: [], + pathLast: 'a', + generics: [ + { + name: 'b', + fullPath: ['b'], + pathWithoutLast: [], + pathLast: 'b', + generics: [], + }, + { + name: 'c', + fullPath: ['c'], + pathWithoutLast: [], + pathLast: 'c', + generics: [], + }, + ], + }, + ], + foundElems: 1, + original: "a<b\tc>", + returned: [], + typeFilter: -1, + userQuery: "a<b\tc>", + error: null, + }, +]; diff --git a/src/test/rustdoc-js-std/parser-weird-queries.js b/src/test/rustdoc-js-std/parser-weird-queries.js new file mode 100644 index 00000000000..a3d85aeca5e --- /dev/null +++ b/src/test/rustdoc-js-std/parser-weird-queries.js @@ -0,0 +1,123 @@ +// This test is mostly to check that the parser still kinda outputs something +// (and doesn't enter an infinite loop!) even though the query is completely +// invalid. +const QUERY = [ + 'a b', + 'a b', + 'a,b(c)', + 'aaa,a', + ',,,,', + 'mod :', + 'mod\t:', +]; + +const PARSED = [ + { + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }, + { + name: "b", + fullPath: ["b"], + pathWithoutLast: [], + pathLast: "b", + generics: [], + }, + ], + foundElems: 2, + original: "a b", + returned: [], + typeFilter: -1, + userQuery: "a b", + error: null, + }, + { + elems: [ + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }, + { + name: "b", + fullPath: ["b"], + pathWithoutLast: [], + pathLast: "b", + generics: [], + }, + ], + foundElems: 2, + original: "a b", + returned: [], + typeFilter: -1, + userQuery: "a b", + error: null, + }, + { + elems: [], + foundElems: 0, + original: "a,b(c)", + returned: [], + typeFilter: -1, + userQuery: "a,b(c)", + error: "Unexpected `(`", + }, + { + elems: [ + { + name: "aaa", + fullPath: ["aaa"], + pathWithoutLast: [], + pathLast: "aaa", + generics: [], + }, + { + name: "a", + fullPath: ["a"], + pathWithoutLast: [], + pathLast: "a", + generics: [], + }, + ], + foundElems: 2, + original: "aaa,a", + returned: [], + typeFilter: -1, + userQuery: "aaa,a", + error: null, + }, + { + elems: [], + foundElems: 0, + original: ",,,,", + returned: [], + typeFilter: -1, + userQuery: ",,,,", + error: null, + }, + { + elems: [], + foundElems: 0, + original: 'mod :', + returned: [], + typeFilter: 0, + userQuery: 'mod :', + error: null, + }, + { + elems: [], + foundElems: 0, + original: 'mod\t:', + returned: [], + typeFilter: 0, + userQuery: 'mod\t:', + error: null, + }, +]; diff --git a/src/test/rustdoc-js-std/quoted.js b/src/test/rustdoc-js-std/quoted.js index 924129f86c8..aec8484a41f 100644 --- a/src/test/rustdoc-js-std/quoted.js +++ b/src/test/rustdoc-js-std/quoted.js @@ -1,4 +1,7 @@ +// ignore-order + const QUERY = '"error"'; +const FILTER_CRATE = 'std'; const EXPECTED = { 'others': [ @@ -6,7 +9,12 @@ const EXPECTED = { { 'path': 'std::fmt', 'name': 'Error' }, { 'path': 'std::io', 'name': 'Error' }, ], - 'in_args': [], + 'in_args': [ + { 'path': 'std::fmt::Error', 'name': 'eq' }, + { 'path': 'std::fmt::Error', 'name': 'cmp' }, + { 'path': 'std::fmt::Error', 'name': 'partial_cmp' }, + + ], 'returned': [ { 'path': 'std::fmt::LowerExp', 'name': 'fmt' }, ], diff --git a/src/test/rustdoc-js-std/struct-vec.js b/src/test/rustdoc-js-std/struct-vec.js index 2c808143bae..29609904b19 100644 --- a/src/test/rustdoc-js-std/struct-vec.js +++ b/src/test/rustdoc-js-std/struct-vec.js @@ -1,8 +1,8 @@ -const QUERY = 'struct:Vec'; +const QUERY = 'struct:VecD'; const EXPECTED = { 'others': [ - { 'path': 'std::vec', 'name': 'Vec' }, { 'path': 'std::collections', 'name': 'VecDeque' }, + { 'path': 'std::vec', 'name': 'Vec' }, ], }; diff --git a/src/test/rustdoc-js-std/typed-query.js b/src/test/rustdoc-js-std/typed-query.js index 3915ee7dc5d..25efbad2695 100644 --- a/src/test/rustdoc-js-std/typed-query.js +++ b/src/test/rustdoc-js-std/typed-query.js @@ -1,6 +1,7 @@ // exact-check const QUERY = 'macro:print'; +const FILTER_CRATE = 'std'; const EXPECTED = { 'others': [ @@ -9,6 +10,8 @@ const EXPECTED = { { 'path': 'std', 'name': 'println' }, { 'path': 'std', 'name': 'eprintln' }, { 'path': 'std::pin', 'name': 'pin' }, - { 'path': 'core::pin', 'name': 'pin' }, + { 'path': 'std::future', 'name': 'join' }, + { 'path': 'std', 'name': 'line' }, + { 'path': 'std', 'name': 'write' }, ], }; diff --git a/src/test/rustdoc-js-std/vec-new.js b/src/test/rustdoc-js-std/vec-new.js index e1a3256876b..cd0e8e7b4a9 100644 --- a/src/test/rustdoc-js-std/vec-new.js +++ b/src/test/rustdoc-js-std/vec-new.js @@ -4,6 +4,6 @@ const EXPECTED = { 'others': [ { 'path': 'std::vec::Vec', 'name': 'new' }, { 'path': 'std::vec::Vec', 'name': 'ne' }, - { 'path': 'std::rc::Rc', 'name': 'ne' }, + { 'path': 'alloc::vec::Vec', 'name': 'ne' }, ], }; diff --git a/src/test/rustdoc-js/doc-alias-filter.js b/src/test/rustdoc-js/doc-alias-filter.js index 4b1e2e29704..e06047ba760 100644 --- a/src/test/rustdoc-js/doc-alias-filter.js +++ b/src/test/rustdoc-js/doc-alias-filter.js @@ -1,6 +1,6 @@ // exact-check -const QUERY = 'true'; +const QUERY = '"true"'; const FILTER_CRATE = 'doc_alias_filter'; diff --git a/src/test/rustdoc-js/doc-alias.js b/src/test/rustdoc-js/doc-alias.js index ff188d51458..7bb0cbe388f 100644 --- a/src/test/rustdoc-js/doc-alias.js +++ b/src/test/rustdoc-js/doc-alias.js @@ -27,6 +27,7 @@ const QUERY = [ const EXPECTED = [ { + // StructItem 'others': [ { 'path': 'doc_alias', @@ -38,6 +39,7 @@ const EXPECTED = [ ], }, { + // StructFieldItem 'others': [ { 'path': 'doc_alias::Struct', @@ -49,6 +51,7 @@ const EXPECTED = [ ], }, { + // StructMethodItem 'others': [ { 'path': 'doc_alias::Struct', @@ -76,6 +79,7 @@ const EXPECTED = [ ], }, { + // ImplTraitFunction 'others': [ { 'path': 'doc_alias::Struct', @@ -87,6 +91,7 @@ const EXPECTED = [ ], }, { + // EnumItem 'others': [ { 'path': 'doc_alias', @@ -98,6 +103,7 @@ const EXPECTED = [ ], }, { + // VariantItem 'others': [ { 'path': 'doc_alias::Enum', @@ -109,6 +115,7 @@ const EXPECTED = [ ], }, { + // EnumMethodItem 'others': [ { 'path': 'doc_alias::Enum', @@ -120,6 +127,7 @@ const EXPECTED = [ ], }, { + // TypedefItem 'others': [ { 'path': 'doc_alias', @@ -131,6 +139,7 @@ const EXPECTED = [ ], }, { + // TraitItem 'others': [ { 'path': 'doc_alias', @@ -142,6 +151,7 @@ const EXPECTED = [ ], }, { + // TraitTypeItem 'others': [ { 'path': 'doc_alias::Trait', @@ -153,6 +163,7 @@ const EXPECTED = [ ], }, { + // AssociatedConstItem 'others': [ { 'path': 'doc_alias::Trait', @@ -164,6 +175,7 @@ const EXPECTED = [ ], }, { + // TraitFunctionItem 'others': [ { 'path': 'doc_alias::Trait', @@ -175,6 +187,7 @@ const EXPECTED = [ ], }, { + // FunctionItem 'others': [ { 'path': 'doc_alias', @@ -186,6 +199,7 @@ const EXPECTED = [ ], }, { + // ModuleItem 'others': [ { 'path': 'doc_alias', @@ -197,6 +211,7 @@ const EXPECTED = [ ], }, { + // ConstItem 'others': [ { 'path': 'doc_alias', @@ -212,6 +227,7 @@ const EXPECTED = [ ], }, { + // StaticItem 'others': [ { 'path': 'doc_alias', @@ -223,6 +239,7 @@ const EXPECTED = [ ], }, { + // UnionItem 'others': [ { 'path': 'doc_alias', @@ -240,6 +257,7 @@ const EXPECTED = [ ], }, { + // UnionFieldItem 'others': [ { 'path': 'doc_alias::Union', @@ -251,6 +269,7 @@ const EXPECTED = [ ], }, { + // UnionMethodItem 'others': [ { 'path': 'doc_alias::Union', @@ -262,6 +281,7 @@ const EXPECTED = [ ], }, { + // MacroItem 'others': [ { 'path': 'doc_alias', diff --git a/src/test/rustdoc-js/generics.js b/src/test/rustdoc-js/generics.js index 63a9ad53812..5e5ba7cd9ac 100644 --- a/src/test/rustdoc-js/generics.js +++ b/src/test/rustdoc-js/generics.js @@ -1,16 +1,18 @@ // exact-check const QUERY = [ - '"R<P>"', + 'R<P>', '"P"', 'P', - '"ExtraCreditStructMulti<ExtraCreditInnerMulti, ExtraCreditInnerMulti>"', + 'ExtraCreditStructMulti<ExtraCreditInnerMulti, ExtraCreditInnerMulti>', 'TraitCat', 'TraitDog', + 'Result<String>', ]; const EXPECTED = [ { + // R<P> 'returned': [ { 'path': 'generics', 'name': 'alef' }, ], @@ -19,6 +21,7 @@ const EXPECTED = [ ], }, { + // "P" 'others': [ { 'path': 'generics', 'name': 'P' }, ], @@ -30,29 +33,41 @@ const EXPECTED = [ ], }, { + // P 'returned': [ { 'path': 'generics', 'name': 'alef' }, - { 'path': 'generics', 'name': 'bet' }, ], 'in_args': [ { 'path': 'generics', 'name': 'alpha' }, - { 'path': 'generics', 'name': 'beta' }, ], }, { + // "ExtraCreditStructMulti"<ExtraCreditInnerMulti, ExtraCreditInnerMulti> 'in_args': [ { 'path': 'generics', 'name': 'extracreditlabhomework' }, ], 'returned': [], }, { + // TraitCat 'in_args': [ { 'path': 'generics', 'name': 'gamma' }, ], }, { + // TraitDog 'in_args': [ { 'path': 'generics', 'name': 'gamma' }, ], }, + { + // Result<String> + 'others': [], + 'returned': [ + { 'path': 'generics', 'name': 'super_soup' }, + ], + 'in_args': [ + { 'path': 'generics', 'name': 'super_soup' }, + ], + }, ]; diff --git a/src/test/rustdoc-js/generics.rs b/src/test/rustdoc-js/generics.rs index 5e11a6d6018..055c51c7ec5 100644 --- a/src/test/rustdoc-js/generics.rs +++ b/src/test/rustdoc-js/generics.rs @@ -24,3 +24,5 @@ pub trait TraitCat {} pub trait TraitDog {} pub fn gamma<T: TraitCat + TraitDog>(t: T) {} + +pub fn super_soup(s: Result<String, i32>) -> Result<String, i32> { s } diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index dbf5cf9650c..17362338355 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -58,7 +58,8 @@ function extractFunction(content, functionName) { } while (pos < content.length && content[pos] !== '/' && content[pos - 1] !== '*'); // Eat quoted strings - } else if (content[pos] === '"' || content[pos] === "'" || content[pos] === "`") { + } else if ((content[pos] === '"' || content[pos] === "'" || content[pos] === "`") && + (pos === 0 || content[pos - 1] !== '/')) { stop = content[pos]; do { if (content[pos] === '\\') { @@ -269,8 +270,13 @@ function loadSearchJsAndIndex(searchJs, searchIndex, storageJs, crate) { // execQuery last parameter is built in buildIndex. // buildIndex requires the hashmap from search-index. var functionsToLoad = ["buildHrefAndPath", "pathSplitter", "levenshtein", "validateResult", - "handleAliases", "getQuery", "buildIndex", "execQuery", "execSearch", - "removeEmptyStringsFromArray"]; + "buildIndex", "execQuery", "parseQuery", "createQueryResults", + "isWhitespace", "isSpecialStartCharacter", "isStopCharacter", + "parseInput", "getItemsBefore", "getNextElem", "createQueryElement", + "isReturnArrow", "isPathStart", "getStringElem", "newParsedQuery", + "itemTypeFromName", "isEndCharacter", "isErrorCharacter", + "isIdentCharacter", "isSeparatorCharacter", "getIdentEndPosition", + "checkExtraTypeFilterCharacters", "isWhitespaceCharacter"]; const functions = ["hasOwnPropertyRustdoc", "onEach"]; ALIASES = {}; @@ -286,12 +292,99 @@ function loadSearchJsAndIndex(searchJs, searchIndex, storageJs, crate) { return [loaded, index]; } +// This function checks if `expected` has all the required fields needed for the checks. +function checkNeededFields(fullPath, expected, error_text, queryName, position) { + let fieldsToCheck; + if (fullPath.length === 0) { + fieldsToCheck = [ + "foundElems", + "original", + "returned", + "typeFilter", + "userQuery", + "error", + ]; + } else if (fullPath.endsWith("elems") || fullPath.endsWith("generics")) { + fieldsToCheck = [ + "name", + "fullPath", + "pathWithoutLast", + "pathLast", + "generics", + ]; + } else { + fieldsToCheck = []; + } + for (var i = 0; i < fieldsToCheck.length; ++i) { + const field = fieldsToCheck[i]; + if (!expected.hasOwnProperty(field)) { + let text = `${queryName}==> Mandatory key \`${field}\` is not present`; + if (fullPath.length > 0) { + text += ` in field \`${fullPath}\``; + if (position != null) { + text += ` (position ${position})`; + } + } + error_text.push(text); + } + } +} + +function valueCheck(fullPath, expected, result, error_text, queryName) { + if (Array.isArray(expected)) { + for (var i = 0; i < expected.length; ++i) { + checkNeededFields(fullPath, expected[i], error_text, queryName, i); + if (i >= result.length) { + error_text.push(`${queryName}==> EXPECTED has extra value in array from field ` + + `\`${fullPath}\` (position ${i}): \`${JSON.stringify(expected[i])}\``); + } else { + valueCheck(fullPath + '[' + i + ']', expected[i], result[i], error_text, queryName); + } + } + for (; i < result.length; ++i) { + error_text.push(`${queryName}==> RESULT has extra value in array from field ` + + `\`${fullPath}\` (position ${i}): \`${JSON.stringify(result[i])}\` ` + + 'compared to EXPECTED'); + } + } else if (expected !== null && typeof expected !== "undefined" && + expected.constructor == Object) + { + for (const key in expected) { + if (!expected.hasOwnProperty(key)) { + continue; + } + if (!result.hasOwnProperty(key)) { + error_text.push('==> Unknown key "' + key + '"'); + break; + } + const obj_path = fullPath + (fullPath.length > 0 ? '.' : '') + key; + valueCheck(obj_path, expected[key], result[key], error_text, queryName); + } + } else { + expectedValue = JSON.stringify(expected); + resultValue = JSON.stringify(result); + if (expectedValue != resultValue) { + error_text.push(`${queryName}==> Different values for field \`${fullPath}\`:\n` + + `EXPECTED: \`${expectedValue}\`\nRESULT: \`${resultValue}\``); + } + } +} + +function runParser(query, expected, loaded, loadedFile, queryName) { + var error_text = []; + checkNeededFields("", expected, error_text, queryName, null); + if (error_text.length === 0) { + valueCheck('', expected, loaded.parseQuery(query), error_text, queryName); + } + return error_text; +} + function runSearch(query, expected, index, loaded, loadedFile, queryName) { const filter_crate = loadedFile.FILTER_CRATE; const ignore_order = loadedFile.ignore_order; const exact_check = loadedFile.exact_check; - var results = loaded.execSearch(loaded.getQuery(query), index, filter_crate); + var results = loaded.execQuery(loaded.parseQuery(query), index, filter_crate); var error_text = []; for (var key in expected) { @@ -353,40 +446,75 @@ function checkResult(error_text, loadedFile, displaySuccess) { return 1; } -function runChecks(testFile, loaded, index) { - var testFileContent = readFile(testFile) + 'exports.QUERY = QUERY;exports.EXPECTED = EXPECTED;'; - if (testFileContent.indexOf("FILTER_CRATE") !== -1) { - testFileContent += "exports.FILTER_CRATE = FILTER_CRATE;"; - } else { - testFileContent += "exports.FILTER_CRATE = null;"; - } - var loadedFile = loadContent(testFileContent); - - const expected = loadedFile.EXPECTED; +function runCheck(loadedFile, key, callback) { + const expected = loadedFile[key]; const query = loadedFile.QUERY; if (Array.isArray(query)) { if (!Array.isArray(expected)) { console.log("FAILED"); - console.log("==> If QUERY variable is an array, EXPECTED should be an array too"); + console.log(`==> If QUERY variable is an array, ${key} should be an array too`); return 1; } else if (query.length !== expected.length) { console.log("FAILED"); - console.log("==> QUERY variable should have the same length as EXPECTED"); + console.log(`==> QUERY variable should have the same length as ${key}`); return 1; } for (var i = 0; i < query.length; ++i) { - var error_text = runSearch(query[i], expected[i], index, loaded, loadedFile, - "[ query `" + query[i] + "`]"); + var error_text = callback(query[i], expected[i], "[ query `" + query[i] + "`]"); if (checkResult(error_text, loadedFile, false) !== 0) { return 1; } } console.log("OK"); - return 0; + } else { + var error_text = callback(query, expected, ""); + if (checkResult(error_text, loadedFile, true) !== 0) { + return 1; + } + } + return 0; +} + +function runChecks(testFile, loaded, index) { + var checkExpected = false; + var checkParsed = false; + var testFileContent = readFile(testFile) + 'exports.QUERY = QUERY;'; + + if (testFileContent.indexOf("FILTER_CRATE") !== -1) { + testFileContent += "exports.FILTER_CRATE = FILTER_CRATE;"; + } else { + testFileContent += "exports.FILTER_CRATE = null;"; + } + + if (testFileContent.indexOf("\nconst EXPECTED") !== -1) { + testFileContent += 'exports.EXPECTED = EXPECTED;'; + checkExpected = true; + } + if (testFileContent.indexOf("\nconst PARSED") !== -1) { + testFileContent += 'exports.PARSED = PARSED;'; + checkParsed = true; + } + if (!checkParsed && !checkExpected) { + console.log("FAILED"); + console.log("==> At least `PARSED` or `EXPECTED` is needed!"); + return 1; + } + + const loadedFile = loadContent(testFileContent); + var res = 0; + + if (checkExpected) { + res += runCheck(loadedFile, "EXPECTED", (query, expected, text) => { + return runSearch(query, expected, index, loaded, loadedFile, text); + }); + } + if (checkParsed) { + res += runCheck(loadedFile, "PARSED", (query, expected, text) => { + return runParser(query, expected, loaded, loadedFile, text); + }); } - var error_text = runSearch(query, expected, index, loaded, loadedFile, ""); - return checkResult(error_text, loadedFile, true); + return res; } function load_files(doc_folder, resource_suffix, crate) { |
