rustdoc: restructure type search engine to pick-and-use IDs

This change makes it so, instead of mixing string distance with
type unification, function signature search works by
mapping names to IDs at the start, reporting to the user any
cases where it had to make corrections, and then matches with
IDs when going through the items.

This only changes function searches. Name searches are left alone,
and corrections are only done when there's a single item in the
search query.
This commit is contained in:
Michael Howell 2023-04-15 11:53:50 -07:00
parent 1a7132d4f8
commit 4c11822aeb
6 changed files with 353 additions and 203 deletions

View File

@ -1259,6 +1259,10 @@ a.tooltip:hover::after {
background-color: var(--search-error-code-background-color);
}
.search-corrections {
font-weight: normal;
}
#src-sidebar-toggle {
position: sticky;
top: 0;

View File

@ -9,6 +9,7 @@ function initSearch(searchIndex){}
/**
* @typedef {{
* name: string,
* id: integer,
* fullPath: Array<string>,
* pathWithoutLast: Array<string>,
* pathLast: string,
@ -36,6 +37,8 @@ let ParserState;
* args: Array<QueryElement>,
* returned: Array<QueryElement>,
* foundElems: number,
* literalSearch: boolean,
* corrections: Array<{from: string, to: integer}>,
* }}
*/
let ParsedQuery;
@ -139,7 +142,7 @@ let FunctionSearchType;
/**
* @typedef {{
* name: (null|string),
* id: (null|number),
* ty: (null|number),
* generics: Array<FunctionType>,
* }}

View File

@ -58,6 +58,7 @@ function printTab(nb) {
}
iter += 1;
});
const isTypeSearch = (nb > 0 || iter === 1);
iter = 0;
onEachLazy(document.getElementById("results").childNodes, elem => {
if (nb === iter) {
@ -70,6 +71,13 @@ function printTab(nb) {
});
if (foundCurrentTab && foundCurrentResultSet) {
searchState.currentTab = nb;
// Corrections only kick in on type-based searches.
const correctionsElem = document.getElementsByClassName("search-corrections");
if (isTypeSearch) {
removeClass(correctionsElem[0], "hidden");
} else {
addClass(correctionsElem[0], "hidden");
}
} else if (nb !== 0) {
printTab(0);
}
@ -191,6 +199,13 @@ function initSearch(rawSearchIndex) {
*/
let searchIndex;
let currentResults;
/**
* Map from normalized type names to integers. Used to make type search
* more efficient.
*
* @type {Map<string, integer>}
*/
let typeNameIdMap;
const ALIASES = new Map();
function isWhitespace(c) {
@ -358,6 +373,7 @@ function initSearch(rawSearchIndex) {
parserState.typeFilter = null;
return {
name: name,
id: -1,
fullPath: pathSegments,
pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1),
pathLast: pathSegments[pathSegments.length - 1],
@ -718,6 +734,7 @@ function initSearch(rawSearchIndex) {
foundElems: 0,
literalSearch: false,
error: null,
correction: null,
};
}
@ -1091,48 +1108,50 @@ function initSearch(rawSearchIndex) {
*
* @param {Row} row - The object to check.
* @param {QueryElement} elem - The element from the parsed query.
* @param {integer} defaultDistance - This is the value to return in case there are no
* generics.
*
* @return {integer} - Returns the best match (if any) or `maxEditDistance + 1`.
* @return {boolean} - Returns true if a match, false otherwise.
*/
function checkGenerics(row, elem, defaultDistance, maxEditDistance) {
if (row.generics.length === 0) {
return elem.generics.length === 0 ? defaultDistance : maxEditDistance + 1;
} else if (row.generics.length > 0 && row.generics[0].name === null) {
return checkGenerics(row.generics[0], elem, defaultDistance, maxEditDistance);
function checkGenerics(row, elem) {
if (row.generics.length === 0 || elem.generics.length === 0) {
return false;
}
// The names match, but we need to be sure that all generics kinda
// match as well.
// This function is called if the names match, but we need to make
// sure that all generics match as well.
//
// This search engine implements order-agnostic unification. There
// should be no missing duplicates (generics have "bag semantics"),
// and the row is allowed to have extras.
if (elem.generics.length > 0 && row.generics.length >= elem.generics.length) {
const elems = new Map();
for (const entry of row.generics) {
if (entry.name === "") {
const addEntryToElems = function addEntryToElems(entry) {
if (entry.id === -1) {
// Pure generic, needs to check into it.
if (checkGenerics(entry, elem, maxEditDistance + 1, maxEditDistance)
!== 0) {
return maxEditDistance + 1;
for (const inner_entry of entry.generics) {
addEntryToElems(inner_entry);
}
continue;
return;
}
let currentEntryElems;
if (elems.has(entry.name)) {
currentEntryElems = elems.get(entry.name);
if (elems.has(entry.id)) {
currentEntryElems = elems.get(entry.id);
} else {
currentEntryElems = [];
elems.set(entry.name, currentEntryElems);
elems.set(entry.id, currentEntryElems);
}
currentEntryElems.push(entry);
};
for (const entry of row.generics) {
addEntryToElems(entry);
}
// We need to find the type that matches the most to remove it in order
// to move forward.
const handleGeneric = generic => {
if (!elems.has(generic.name)) {
if (!elems.has(generic.id)) {
return false;
}
const matchElems = elems.get(generic.name);
const matchElems = elems.get(generic.id);
const matchIdx = matchElems.findIndex(tmp_elem => {
if (checkGenerics(tmp_elem, generic, 0, maxEditDistance) !== 0) {
if (generic.generics.length > 0 && !checkGenerics(tmp_elem, generic)) {
return false;
}
return typePassesFilter(generic.typeFilter, tmp_elem.ty);
@ -1142,7 +1161,7 @@ function initSearch(rawSearchIndex) {
}
matchElems.splice(matchIdx, 1);
if (matchElems.length === 0) {
elems.delete(generic.name);
elems.delete(generic.id);
}
return true;
};
@ -1152,17 +1171,17 @@ function initSearch(rawSearchIndex) {
// own type.
for (const generic of elem.generics) {
if (generic.typeFilter !== -1 && !handleGeneric(generic)) {
return maxEditDistance + 1;
return false;
}
}
for (const generic of elem.generics) {
if (generic.typeFilter === -1 && !handleGeneric(generic)) {
return maxEditDistance + 1;
return false;
}
}
return 0;
return true;
}
return maxEditDistance + 1;
return false;
}
/**
@ -1172,17 +1191,15 @@ function initSearch(rawSearchIndex) {
* @param {Row} row
* @param {QueryElement} elem - The element from the parsed query.
*
* @return {integer} - Returns an edit distance to the best match.
* @return {boolean} - Returns true if found, false otherwise.
*/
function checkIfInGenerics(row, elem, maxEditDistance) {
let dist = maxEditDistance + 1;
function checkIfInGenerics(row, elem) {
for (const entry of row.generics) {
dist = Math.min(checkType(entry, elem, true, maxEditDistance), dist);
if (dist === 0) {
break;
if (checkType(entry, elem)) {
return true;
}
}
return dist;
return false;
}
/**
@ -1191,75 +1208,30 @@ function initSearch(rawSearchIndex) {
*
* @param {Row} row
* @param {QueryElement} elem - The element from the parsed query.
* @param {boolean} literalSearch
*
* @return {integer} - Returns an edit distance to the best match. If there is
* no match, returns `maxEditDistance + 1`.
* @return {boolean} - Returns true if the type matches, false otherwise.
*/
function checkType(row, elem, literalSearch, maxEditDistance) {
if (row.name === null) {
function checkType(row, elem) {
if (row.id === -1) {
// This is a pure "generic" search, no need to run other checks.
if (row.generics.length > 0) {
return checkIfInGenerics(row, elem, maxEditDistance);
}
return maxEditDistance + 1;
return row.generics.length > 0 ? checkIfInGenerics(row, elem) : false;
}
let dist;
if (typePassesFilter(elem.typeFilter, row.ty)) {
dist = editDistance(row.name, elem.name, maxEditDistance);
} else {
dist = maxEditDistance + 1;
if (row.id === elem.id && typePassesFilter(elem.typeFilter, row.ty)) {
if (elem.generics.length > 0) {
return checkGenerics(row, elem);
}
if (literalSearch) {
if (dist !== 0) {
// The name didn't match, let's try to check if the generics do.
if (elem.generics.length === 0) {
const checkGeneric = row.generics.length > 0;
if (checkGeneric && row.generics
.findIndex(tmp_elem => tmp_elem.name === elem.name &&
typePassesFilter(elem.typeFilter, tmp_elem.ty)) !== -1) {
return 0;
return true;
}
// If the current item does not match, try [unboxing] the generic.
// [unboxing]:
// https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf
if (checkIfInGenerics(row, elem)) {
return true;
}
return maxEditDistance + 1;
} else if (elem.generics.length > 0) {
return checkGenerics(row, elem, maxEditDistance + 1, maxEditDistance);
}
return 0;
} else if (row.generics.length > 0) {
if (elem.generics.length === 0) {
if (dist === 0) {
return 0;
}
// The name didn't match so we now check if the type we're looking for is inside
// the generics!
dist = Math.min(dist, checkIfInGenerics(row, elem, maxEditDistance));
return dist;
} else if (dist > maxEditDistance) {
// 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, maxEditDistance);
} else {
// At this point, the name kinda match and we have generics to check, so
// let's go!
const tmp_dist = checkGenerics(row, elem, dist, maxEditDistance);
if (tmp_dist > maxEditDistance) {
return maxEditDistance + 1;
}
// We compute the median value of both checks and return it.
return (tmp_dist + dist) / 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 maxEditDistance + 1;
}
// No generics on our query or on the target type so we can return without doing
// anything else.
return dist;
return false;
}
/**
@ -1267,17 +1239,11 @@ function initSearch(rawSearchIndex) {
*
* @param {Row} row
* @param {QueryElement} elem - The element from the parsed query.
* @param {integer} maxEditDistance
* @param {Array<integer>} skipPositions - Do not return one of these positions.
*
* @return {dist: integer, position: integer} - Returns an edit distance to the best match.
* If there is no match, returns
* `maxEditDistance + 1` and position: -1.
* @return {integer} - Returns the position of the match, or -1 if none.
*/
function findArg(row, elem, maxEditDistance, skipPositions) {
let dist = maxEditDistance + 1;
let position = -1;
function findArg(row, elem, skipPositions) {
if (row && row.type && row.type.inputs && row.type.inputs.length > 0) {
let i = 0;
for (const input of row.type.inputs) {
@ -1285,24 +1251,13 @@ function initSearch(rawSearchIndex) {
i += 1;
continue;
}
const typeDist = checkType(
input,
elem,
parsedQuery.literalSearch,
maxEditDistance
);
if (typeDist === 0) {
return {dist: 0, position: i};
}
if (typeDist < dist) {
dist = typeDist;
position = i;
if (checkType(input, elem)) {
return i;
}
i += 1;
}
}
dist = parsedQuery.literalSearch ? maxEditDistance + 1 : dist;
return {dist, position};
return -1;
}
/**
@ -1310,43 +1265,25 @@ function initSearch(rawSearchIndex) {
*
* @param {Row} row
* @param {QueryElement} elem - The element from the parsed query.
* @param {integer} maxEditDistance
* @param {Array<integer>} skipPositions - Do not return one of these positions.
*
* @return {dist: integer, position: integer} - Returns an edit distance to the best match.
* If there is no match, returns
* `maxEditDistance + 1` and position: -1.
* @return {integer} - Returns the position of the matching item, or -1 if none.
*/
function checkReturned(row, elem, maxEditDistance, skipPositions) {
let dist = maxEditDistance + 1;
let position = -1;
function checkReturned(row, elem, skipPositions) {
if (row && row.type && row.type.output.length > 0) {
const ret = row.type.output;
let i = 0;
for (const ret_ty of ret) {
for (const ret_ty of row.type.output) {
if (skipPositions.indexOf(i) !== -1) {
i += 1;
continue;
}
const typeDist = checkType(
ret_ty,
elem,
parsedQuery.literalSearch,
maxEditDistance
);
if (typeDist === 0) {
return {dist: 0, position: i};
}
if (typeDist < dist) {
dist = typeDist;
position = i;
if (checkType(ret_ty, elem)) {
return i;
}
i += 1;
}
}
dist = parsedQuery.literalSearch ? maxEditDistance + 1 : dist;
return {dist, position};
return -1;
}
function checkPath(contains, ty, maxEditDistance) {
@ -1543,17 +1480,20 @@ function initSearch(rawSearchIndex) {
if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
return;
}
let dist, index = -1, path_dist = 0;
let index = -1, path_dist = 0;
const fullId = row.id;
const searchWord = searchWords[pos];
const in_args = findArg(row, elem, maxEditDistance, []);
const returned = checkReturned(row, elem, maxEditDistance, []);
const in_args = findArg(row, elem, []);
if (in_args !== -1) {
// path_dist is 0 because no parent path information is currently stored
// in the search index
addIntoResults(results_in_args, fullId, pos, -1, in_args.dist, 0, maxEditDistance);
addIntoResults(results_returned, fullId, pos, -1, returned.dist, 0, maxEditDistance);
addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance);
}
const returned = checkReturned(row, elem, []);
if (returned !== -1) {
addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance);
}
if (!typePassesFilter(elem.typeFilter, row.ty)) {
return;
@ -1574,16 +1514,6 @@ function initSearch(rawSearchIndex) {
index = row_index;
}
// No need to check anything else if it's a "pure" generics search.
if (elem.name.length === 0) {
if (row.type !== null) {
dist = checkGenerics(row.type, elem, maxEditDistance + 1, maxEditDistance);
// path_dist is 0 because we know it's empty
addIntoResults(results_others, fullId, pos, index, dist, 0, maxEditDistance);
}
return;
}
if (elem.fullPath.length > 1) {
path_dist = checkPath(elem.pathWithoutLast, row, maxEditDistance);
if (path_dist > maxEditDistance) {
@ -1598,7 +1528,7 @@ function initSearch(rawSearchIndex) {
return;
}
dist = editDistance(searchWord, elem.pathLast, maxEditDistance);
const dist = editDistance(searchWord, elem.pathLast, maxEditDistance);
if (index === -1 && dist + path_dist > maxEditDistance) {
return;
@ -1616,28 +1546,22 @@ function initSearch(rawSearchIndex) {
* @param {integer} pos - Position in the `searchIndex`.
* @param {Object} results
*/
function handleArgs(row, pos, results, maxEditDistance) {
function handleArgs(row, pos, results) {
if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
return;
}
let totalDist = 0;
let nbDist = 0;
// If the result is too "bad", we return false and it ends this search.
function checkArgs(elems, callback) {
const skipPositions = [];
for (const elem of elems) {
// There is more than one parameter to the query so all checks should be "exact"
const { dist, position } = callback(
const position = callback(
row,
elem,
maxEditDistance,
skipPositions
);
if (dist <= 1) {
nbDist += 1;
totalDist += dist;
if (position !== -1) {
skipPositions.push(position);
} else {
return false;
@ -1652,11 +1576,7 @@ function initSearch(rawSearchIndex) {
return;
}
if (nbDist === 0) {
return;
}
const dist = Math.round(totalDist / nbDist);
addIntoResults(results, row.id, pos, 0, dist, 0, maxEditDistance);
addIntoResults(results, row.id, pos, 0, 0, 0, Number.MAX_VALUE);
}
function innerRunQuery() {
@ -1671,6 +1591,50 @@ function initSearch(rawSearchIndex) {
}
const maxEditDistance = Math.floor(queryLen / 3);
/**
* Convert names to ids in parsed query elements.
* This is not used for the "In Names" tab, but is used for the
* "In Params", "In Returns", and "In Function Signature" tabs.
*
* If there is no matching item, but a close-enough match, this
* function also that correction.
*
* See `buildTypeMapIndex` for more information.
*
* @param {QueryElement} elem
*/
function convertNameToId(elem) {
if (typeNameIdMap.has(elem.name)) {
elem.id = typeNameIdMap.get(elem.name);
} else if (!parsedQuery.literalSearch) {
let match = -1;
let matchDist = maxEditDistance + 1;
let matchName = "";
for (const [name, id] of typeNameIdMap) {
const dist = editDistance(name, elem.name, maxEditDistance);
if (dist <= matchDist && dist <= maxEditDistance) {
match = id;
matchDist = dist;
matchName = name;
}
}
if (match !== -1) {
parsedQuery.correction = matchName;
}
elem.id = match;
}
for (const elem2 of elem.generics) {
convertNameToId(elem2);
}
}
for (const elem of parsedQuery.elems) {
convertNameToId(elem);
}
for (const elem of parsedQuery.returned) {
convertNameToId(elem);
}
if (parsedQuery.foundElems === 1) {
if (parsedQuery.elems.length === 1) {
elem = parsedQuery.elems[0];
@ -1695,22 +1659,23 @@ function initSearch(rawSearchIndex) {
in_returned = checkReturned(
row,
elem,
maxEditDistance,
[]
);
if (in_returned !== -1) {
addIntoResults(
results_others,
row.id,
i,
-1,
in_returned.dist,
maxEditDistance
0,
Number.MAX_VALUE
);
}
}
}
} else if (parsedQuery.foundElems > 0) {
for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) {
handleArgs(searchIndex[i], i, results_others, maxEditDistance);
handleArgs(searchIndex[i], i, results_others);
}
}
}
@ -2030,6 +1995,11 @@ function initSearch(rawSearchIndex) {
currentTab = 0;
}
if (results.query.correction !== null) {
output += "<h3 class=\"search-corrections\">Showing results for " +
`"${results.query.correction}".</h3>`;
}
const resultsElem = document.createElement("div");
resultsElem.id = "results";
resultsElem.appendChild(ret_others[0]);
@ -2108,6 +2078,34 @@ 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<string, integer>} 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.
*
@ -2126,7 +2124,7 @@ function initSearch(rawSearchIndex) {
*
* @return {Array<FunctionSearchType>}
*/
function buildItemSearchTypeAll(types, lowercasePaths) {
function buildItemSearchTypeAll(types, lowercasePaths, typeNameIdMap) {
const PATH_INDEX_DATA = 0;
const GENERICS_DATA = 1;
return types.map(type => {
@ -2136,11 +2134,17 @@ function initSearch(rawSearchIndex) {
generics = [];
} else {
pathIndex = type[PATH_INDEX_DATA];
generics = buildItemSearchTypeAll(type[GENERICS_DATA], lowercasePaths);
generics = buildItemSearchTypeAll(
type[GENERICS_DATA],
lowercasePaths,
typeNameIdMap
);
}
return {
// `0` is used as a sentinel because it's fewer bytes than `null`
name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name,
id: pathIndex === 0
? -1
: buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name),
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
generics: generics,
};
@ -2159,10 +2163,11 @@ function initSearch(rawSearchIndex) {
*
* @param {RawFunctionSearchType} functionSearchType
* @param {Array<{name: string, ty: number}>} lowercasePaths
* @param {Map<string, integer>}
*
* @return {null|FunctionSearchType}
*/
function buildFunctionSearchType(functionSearchType, lowercasePaths) {
function buildFunctionSearchType(functionSearchType, lowercasePaths, typeNameIdMap) {
const INPUTS_DATA = 0;
const OUTPUT_DATA = 1;
// `0` is used as a sentinel because it's fewer bytes than `null`
@ -2173,23 +2178,35 @@ function initSearch(rawSearchIndex) {
if (typeof functionSearchType[INPUTS_DATA] === "number") {
const pathIndex = functionSearchType[INPUTS_DATA];
inputs = [{
name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name,
id: pathIndex === 0
? -1
: buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name),
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
generics: [],
}];
} else {
inputs = buildItemSearchTypeAll(functionSearchType[INPUTS_DATA], lowercasePaths);
inputs = buildItemSearchTypeAll(
functionSearchType[INPUTS_DATA],
lowercasePaths,
typeNameIdMap
);
}
if (functionSearchType.length > 1) {
if (typeof functionSearchType[OUTPUT_DATA] === "number") {
const pathIndex = functionSearchType[OUTPUT_DATA];
output = [{
name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name,
id: pathIndex === 0
? -1
: buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name),
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
generics: [],
}];
} else {
output = buildItemSearchTypeAll(functionSearchType[OUTPUT_DATA], lowercasePaths);
output = buildItemSearchTypeAll(
functionSearchType[OUTPUT_DATA],
lowercasePaths,
typeNameIdMap
);
}
} else {
output = [];
@ -2202,9 +2219,12 @@ function initSearch(rawSearchIndex) {
function buildIndex(rawSearchIndex) {
searchIndex = [];
/**
* List of normalized search words (ASCII lowercased, and undescores removed).
*
* @type {Array<string>}
*/
const searchWords = [];
typeNameIdMap = new Map();
const charA = "A".charCodeAt(0);
let currentIndex = 0;
let id = 0;
@ -2337,7 +2357,11 @@ function initSearch(rawSearchIndex) {
path: itemPaths.has(i) ? itemPaths.get(i) : lastPath,
desc: itemDescs[i],
parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined,
type: buildFunctionSearchType(itemFunctionSearchTypes[i], lowercasePaths),
type: buildFunctionSearchType(
itemFunctionSearchTypes[i],
lowercasePaths,
typeNameIdMap
),
id: id,
normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""),
deprecated: deprecatedItems.has(i),

View File

@ -226,6 +226,24 @@ function runSearch(query, expected, doSearch, loadedFile, queryName) {
return error_text;
}
function runCorrections(query, corrections, getCorrections, loadedFile) {
const qc = getCorrections(query, loadedFile.FILTER_CRATE);
const error_text = [];
if (corrections === null) {
if (qc !== null) {
error_text.push(`==> expected = null, found = ${qc}`);
}
return error_text;
}
if (qc !== corrections.toLowerCase()) {
error_text.push(`==> expected = ${corrections}, found = ${qc}`);
}
return error_text;
}
function checkResult(error_text, loadedFile, displaySuccess) {
if (error_text.length === 0 && loadedFile.should_fail === true) {
console.log("FAILED");
@ -272,9 +290,10 @@ function runCheck(loadedFile, key, callback) {
return 0;
}
function runChecks(testFile, doSearch, parseQuery) {
function runChecks(testFile, doSearch, parseQuery, getCorrections) {
let checkExpected = false;
let checkParsed = false;
let checkCorrections = false;
let testFileContent = readFile(testFile) + "exports.QUERY = QUERY;";
if (testFileContent.indexOf("FILTER_CRATE") !== -1) {
@ -291,9 +310,13 @@ function runChecks(testFile, doSearch, parseQuery) {
testFileContent += "exports.PARSED = PARSED;";
checkParsed = true;
}
if (!checkParsed && !checkExpected) {
if (testFileContent.indexOf("\nconst CORRECTIONS") !== -1) {
testFileContent += "exports.CORRECTIONS = CORRECTIONS;";
checkCorrections = true;
}
if (!checkParsed && !checkExpected && !checkCorrections) {
console.log("FAILED");
console.log("==> At least `PARSED` or `EXPECTED` is needed!");
console.log("==> At least `PARSED`, `EXPECTED`, or `CORRECTIONS` is needed!");
return 1;
}
@ -310,6 +333,11 @@ function runChecks(testFile, doSearch, parseQuery) {
return runParser(query, expected, parseQuery, text);
});
}
if (checkCorrections) {
res += runCheck(loadedFile, "CORRECTIONS", (query, expected) => {
return runCorrections(query, expected, getCorrections, loadedFile);
});
}
return res;
}
@ -318,9 +346,10 @@ function runChecks(testFile, doSearch, parseQuery) {
*
* @param {string} doc_folder - Path to a folder generated by running rustdoc
* @param {string} resource_suffix - Version number between filename and .js, e.g. "1.59.0"
* @returns {Object} - Object containing two keys: `doSearch`, which runs a search
* with the loaded index and returns a table of results; and `parseQuery`, which is the
* `parseQuery` function exported from the search module.
* @returns {Object} - Object containing keys: `doSearch`, which runs a search
* with the loaded index and returns a table of results; `parseQuery`, which is the
* `parseQuery` function exported from the search module; and `getCorrections`, which runs
* a search but returns type name corrections instead of results.
*/
function loadSearchJS(doc_folder, resource_suffix) {
const searchIndexJs = path.join(doc_folder, "search-index" + resource_suffix + ".js");
@ -336,6 +365,12 @@ function loadSearchJS(doc_folder, resource_suffix) {
return searchModule.execQuery(searchModule.parseQuery(queryStr), searchWords,
filterCrate, currentCrate);
},
getCorrections: function(queryStr, filterCrate, currentCrate) {
const parsedQuery = searchModule.parseQuery(queryStr);
searchModule.execQuery(parsedQuery, searchWords,
filterCrate, currentCrate);
return parsedQuery.correction;
},
parseQuery: searchModule.parseQuery,
};
}
@ -417,11 +452,14 @@ function main(argv) {
const doSearch = function(queryStr, filterCrate) {
return parseAndSearch.doSearch(queryStr, filterCrate, opts["crate_name"]);
};
const getCorrections = function(queryStr, filterCrate) {
return parseAndSearch.getCorrections(queryStr, filterCrate, opts["crate_name"]);
};
if (opts["test_file"].length !== 0) {
opts["test_file"].forEach(file => {
process.stdout.write(`Testing ${file} ... `);
errors += runChecks(file, doSearch, parseAndSearch.parseQuery);
errors += runChecks(file, doSearch, parseAndSearch.parseQuery, getCorrections);
});
} else if (opts["test_folder"].length !== 0) {
fs.readdirSync(opts["test_folder"]).forEach(file => {
@ -430,7 +468,7 @@ function main(argv) {
}
process.stdout.write(`Testing ${file} ... `);
errors += runChecks(path.join(opts["test_folder"], file), doSearch,
parseAndSearch.parseQuery);
parseAndSearch.parseQuery, getCorrections);
});
}
return errors > 0 ? 1 : 0;

View File

@ -0,0 +1,54 @@
// Checks that the search tab result tell the user about corrections
// First, try a search-by-name
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
// Intentionally wrong spelling of "NotableStructWithLongName"
write: (".search-input", "NotableStructWithLongNamr")
// To be SURE that the search will be run.
press-key: 'Enter'
// Waiting for the search results to appear...
wait-for: "#search-tabs"
// Corrections aren't shown on the "In Names" tab.
assert: "#search-tabs button.selected:first-child"
assert-css: (".search-corrections", {
"display": "none"
})
// Corrections do get shown on the "In Parameters" tab.
click: "#search-tabs button:nth-child(2)"
assert: "#search-tabs button.selected:nth-child(2)"
assert-css: (".search-corrections", {
"display": "block"
})
assert-text: (
".search-corrections",
"Showing results for \"notablestructwithlongname\"."
)
// Corrections do get shown on the "In Return Type" tab.
click: "#search-tabs button:nth-child(3)"
assert: "#search-tabs button.selected:nth-child(3)"
assert-css: (".search-corrections", {
"display": "block"
})
assert-text: (
".search-corrections",
"Showing results for \"notablestructwithlongname\"."
)
// Now, explicit return values
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
// Intentionally wrong spelling of "NotableStructWithLongName"
write: (".search-input", "-> NotableStructWithLongNamr")
// To be SURE that the search will be run.
press-key: 'Enter'
// Waiting for the search results to appear...
wait-for: "#search-tabs"
assert-css: (".search-corrections", {
"display": "block"
})
assert-text: (
".search-corrections",
"Showing results for \"notablestructwithlongname\"."
)

View File

@ -1,9 +1,21 @@
// exact-check
const QUERY = [
'Result<SomeTrait>',
'Result<SomeTraiz>',
'OtherThingxxxxxxxx',
'OtherThingxxxxxxxy',
];
const CORRECTIONS = [
null,
null,
null,
'OtherThingxxxxxxxx',
];
const EXPECTED = [
// Result<SomeTrait>
{
'in_args': [
{ 'path': 'generics_trait', 'name': 'beta' },
@ -12,6 +24,21 @@ const EXPECTED = [
{ 'path': 'generics_trait', 'name': 'bet' },
],
},
// Result<SomeTraiz>
{
'in_args': [],
'returned': [],
},
// OtherThingxxxxxxxx
{
'in_args': [
{ 'path': 'generics_trait', 'name': 'alpha' },
],
'returned': [
{ 'path': 'generics_trait', 'name': 'alef' },
],
},
// OtherThingxxxxxxxy
{
'in_args': [
{ 'path': 'generics_trait', 'name': 'alpha' },