From 47b138b2b89f91278b7825569b1bdad24f1eee6a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 15 Jul 2025 18:11:07 +0200 Subject: [PATCH 1/4] Make aliases search support partial matching --- src/librustdoc/html/render/search_index.rs | 2 +- src/librustdoc/html/static/js/rustdoc.d.ts | 5 + src/librustdoc/html/static/js/search.js | 150 +++++++++++---------- 3 files changed, 83 insertions(+), 74 deletions(-) diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index aff8684ee3a09..ef0a8361fd90a 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -116,7 +116,7 @@ pub(crate) fn build_index( // Set up alias indexes. for (i, item) in cache.search_index.iter().enumerate() { for alias in &item.aliases[..] { - aliases.entry(alias.as_str().to_lowercase()).or_default().push(i); + aliases.entry(alias.to_string()).or_default().push(i); } } diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts index ca2512e5ab688..9ce24d1c06f60 100644 --- a/src/librustdoc/html/static/js/rustdoc.d.ts +++ b/src/librustdoc/html/static/js/rustdoc.d.ts @@ -227,6 +227,11 @@ declare namespace rustdoc { path: string, ty: number, type: FunctionSearchType | null, + descIndex: number, + bitIndex: number, + implDisambiguator: String | null, + is_alias?: boolean, + original?: Row, } /** diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 15cad31f555a6..2caf214ff73d4 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -830,7 +830,7 @@ function createQueryElement(query, parserState, name, generics, isInGenerics) { */ function makePrimitiveElement(name, extra) { return Object.assign({ - name: name, + name, id: null, fullPath: [name], pathWithoutLast: [], @@ -1483,6 +1483,7 @@ class DocSearch { */ this.assocTypeIdNameMap = new Map(); this.ALIASES = new Map(); + this.FOUND_ALIASES = new Set(); this.rootPath = rootPath; this.searchState = searchState; @@ -2030,6 +2031,8 @@ class DocSearch { // normalized names, type signature objects and fingerprints, and aliases. id = 0; + /** @type {Array<[string, { [key: string]: Array }, number]>} */ + const allAliases = []; for (const [crate, crateCorpus] of rawSearchIndex) { // a string representing the lengths of each description shard // a string representing the list of function types @@ -2178,10 +2181,10 @@ class DocSearch { paths[i] = { ty, name, path, exactPath, unboxFlag }; } - // convert `item*` into an object form, and construct word indices. + // Convert `item*` into an object form, and construct word indices. // - // before any analysis is performed lets gather the search terms to - // search against apart from the rest of the data. This is a quick + // Before any analysis is performed, let's gather the search terms to + // search against apart from the rest of the data. This is a quick // operation that is cached for the life of the page state so that // all other search operations have access to this cached data for // faster analysis operations @@ -2269,29 +2272,58 @@ class DocSearch { } if (aliases) { - const currentCrateAliases = new Map(); - this.ALIASES.set(crate, currentCrateAliases); - for (const alias_name in aliases) { - if (!Object.prototype.hasOwnProperty.call(aliases, alias_name)) { - continue; - } - - /** @type{number[]} */ - let currentNameAliases; - if (currentCrateAliases.has(alias_name)) { - currentNameAliases = currentCrateAliases.get(alias_name); - } else { - currentNameAliases = []; - currentCrateAliases.set(alias_name, currentNameAliases); - } - for (const local_alias of aliases[alias_name]) { - currentNameAliases.push(local_alias + currentIndex); - } - } + // We need to add the aliases in `searchIndex` after we finished filling it + // to not mess up indexes. + allAliases.push([crate, aliases, currentIndex]); } currentIndex += itemTypes.length; this.searchState.descShards.set(crate, descShardList); } + + for (const [crate, aliases, index] of allAliases) { + for (const [alias_name, alias_refs] of Object.entries(aliases)) { + if (!this.ALIASES.has(crate)) { + this.ALIASES.set(crate, new Map()); + } + const word = alias_name.toLowerCase(); + const crate_alias_map = this.ALIASES.get(crate); + if (!crate_alias_map.has(word)) { + crate_alias_map.set(word, []); + } + const aliases_map = crate_alias_map.get(word); + + const normalizedName = word.indexOf("_") === -1 ? word : word.replace(/_/g, ""); + for (const alias of alias_refs) { + const originalIndex = alias + index; + const original = searchIndex[originalIndex]; + /** @type {rustdoc.Row} */ + const row = { + crate, + name: alias_name, + normalizedName, + is_alias: true, + ty: original.ty, + type: original.type, + paramNames: [], + word, + id, + parent: undefined, + original, + path: "", + implDisambiguator: original.implDisambiguator, + // Needed to load the description of the original item. + // @ts-ignore + descShard: original.descShard, + descIndex: original.descIndex, + bitIndex: original.bitIndex, + }; + aliases_map.push(row); + this.nameTrie.insert(normalizedName, id, this.tailTable); + id += 1; + searchIndex.push(row); + } + } + } // Drop the (rather large) hash table used for reusing function items this.TYPES_POOL = new Map(); return searchIndex; @@ -2536,6 +2568,8 @@ class DocSearch { parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) + parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0); const maxEditDistance = Math.floor(queryLen / 3); + // We reinitialize the `FOUND_ALIASES` map. + this.FOUND_ALIASES.clear(); /** * @type {Map} @@ -2695,6 +2729,10 @@ class DocSearch { const buildHrefAndPath = item => { let displayPath; let href; + if (item.is_alias) { + this.FOUND_ALIASES.add(item.word); + item = item.original; + } const type = itemTypes[item.ty]; const name = item.name; let path = item.path; @@ -3198,8 +3236,7 @@ class DocSearch { result.item = this.searchIndex[result.id]; result.word = this.searchIndex[result.id].word; if (isReturnTypeQuery) { - // we are doing a return-type based search, - // deprioritize "clone-like" results, + // We are doing a return-type based search, deprioritize "clone-like" results, // ie. functions that also take the queried type as an argument. const resultItemType = result.item && result.item.type; if (!resultItemType) { @@ -4259,28 +4296,13 @@ class DocSearch { return false; } - // this does not yet have a type in `rustdoc.d.ts`. - // @ts-expect-error - function createAliasFromItem(item) { - return { - crate: item.crate, - name: item.name, - path: item.path, - descShard: item.descShard, - descIndex: item.descIndex, - exactPath: item.exactPath, - ty: item.ty, - parent: item.parent, - type: item.type, - is_alias: true, - bitIndex: item.bitIndex, - implDisambiguator: item.implDisambiguator, - }; - } - // @ts-expect-error const handleAliases = async(ret, query, filterCrates, currentCrate) => { const lowerQuery = query.toLowerCase(); + if (this.FOUND_ALIASES.has(lowerQuery)) { + return; + } + this.FOUND_ALIASES.add(lowerQuery); // We separate aliases and crate aliases because we want to have current crate // aliases to be before the others in the displayed results. // @ts-expect-error @@ -4292,7 +4314,7 @@ class DocSearch { && this.ALIASES.get(filterCrates).has(lowerQuery)) { const query_aliases = this.ALIASES.get(filterCrates).get(lowerQuery); for (const alias of query_aliases) { - aliases.push(createAliasFromItem(this.searchIndex[alias])); + aliases.push(alias); } } } else { @@ -4302,7 +4324,7 @@ class DocSearch { const pushTo = crate === currentCrate ? crateAliases : aliases; const query_aliases = crateAliasesIndex.get(lowerQuery); for (const alias of query_aliases) { - pushTo.push(createAliasFromItem(this.searchIndex[alias])); + pushTo.push(alias); } } } @@ -4310,9 +4332,9 @@ class DocSearch { // @ts-expect-error const sortFunc = (aaa, bbb) => { - if (aaa.path < bbb.path) { + if (aaa.original.path < bbb.original.path) { return 1; - } else if (aaa.path === bbb.path) { + } else if (aaa.original.path === bbb.original.path) { return 0; } return -1; @@ -4321,21 +4343,10 @@ class DocSearch { crateAliases.sort(sortFunc); aliases.sort(sortFunc); - // @ts-expect-error - const fetchDesc = alias => { - // @ts-expect-error - return this.searchIndexEmptyDesc.get(alias.crate).contains(alias.bitIndex) ? - "" : this.searchState.loadDesc(alias); - }; - const [crateDescs, descs] = await Promise.all([ - // @ts-expect-error - Promise.all(crateAliases.map(fetchDesc)), - Promise.all(aliases.map(fetchDesc)), - ]); - // @ts-expect-error const pushFunc = alias => { - alias.alias = query; + // Cloning `alias` to prevent its fields to be updated. + alias = {...alias}; const res = buildHrefAndPath(alias); alias.displayPath = pathSplitter(res[0]); alias.fullPath = alias.displayPath + alias.name; @@ -4347,16 +4358,8 @@ class DocSearch { } }; - aliases.forEach((alias, i) => { - // @ts-expect-error - alias.desc = descs[i]; - }); aliases.forEach(pushFunc); // @ts-expect-error - crateAliases.forEach((alias, i) => { - alias.desc = crateDescs[i]; - }); - // @ts-expect-error crateAliases.forEach(pushFunc); }; @@ -4802,7 +4805,7 @@ async function addTab(array, query, display) { output.className = "search-results " + extraClass; const lis = Promise.all(array.map(async item => { - const name = item.name; + const name = item.is_alias ? item.original.name : item.name; const type = itemTypes[item.ty]; const longType = longItemTypes[item.ty]; const typeName = longType.length !== 0 ? `${longType}` : "?"; @@ -4822,7 +4825,7 @@ async function addTab(array, query, display) { let alias = " "; if (item.is_alias) { alias = `
\ -${item.alias} - see \ +${item.name} - see \
`; } resultName.insertAdjacentHTML( @@ -5201,6 +5204,7 @@ function registerSearchEvents() { if (searchState.input.value.length === 0) { searchState.hideResults(); } else { + // @ts-ignore searchState.timeout = setTimeout(search, 500); } }; @@ -5842,8 +5846,8 @@ Lev1TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new In // be called ONLY when the whole file has been parsed and loaded. // @ts-expect-error -function initSearch(searchIndx) { - rawSearchIndex = searchIndx; +function initSearch(searchIndex) { + rawSearchIndex = searchIndex; if (typeof window !== "undefined") { // @ts-expect-error docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState); From b0bf51f9a9323ca5abf595c6d0653d60fe68ee24 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 15 Jul 2025 21:07:55 +0200 Subject: [PATCH 2/4] Update rustdoc search tester to new alias output --- src/tools/rustdoc-js/tester.js | 71 ++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index f70fc917770c6..0baa179e16b2d 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -28,7 +28,14 @@ function readFile(filePath) { } function contentToDiffLine(key, value) { - return `"${key}": "${value}",`; + if (typeof value === "object" && !Array.isArray(value) && value !== null) { + const out = Object.entries(value) + .filter(([subKey, _]) => ["path", "name"].includes(subKey)) + .map(([subKey, subValue]) => `"${subKey}": ${JSON.stringify(subValue)}`) + .join(", "); + return `"${key}": ${out},`; + } + return `"${key}": ${JSON.stringify(value)},`; } function shouldIgnoreField(fieldName) { @@ -37,47 +44,61 @@ function shouldIgnoreField(fieldName) { fieldName === "proposeCorrectionTo"; } +function valueMapper(key, testOutput) { + const isAlias = testOutput["is_alias"]; + let value = testOutput[key]; + // To make our life easier, if there is a "parent" type, we add it to the path. + if (key === "path") { + if (testOutput["parent"] !== undefined) { + if (value.length > 0) { + value += "::" + testOutput["parent"]["name"]; + } else { + value = testOutput["parent"]["name"]; + } + } else if (testOutput["is_alias"]) { + value = valueMapper(key, testOutput["original"]); + } + } else if (isAlias && key === "alias") { + value = testOutput["name"]; + } else if (isAlias && ["name"].includes(key)) { + value = testOutput["original"][key]; + } + return value; +} + // This function is only called when no matching result was found and therefore will only display // the diff between the two items. -function betterLookingDiff(entry, data) { +function betterLookingDiff(expected, testOutput) { let output = " {\n"; - const spaces = " "; - for (const key in entry) { - if (!Object.prototype.hasOwnProperty.call(entry, key)) { + const spaces = " "; + for (const key in expected) { + if (!Object.prototype.hasOwnProperty.call(expected, key)) { continue; } - if (!data || !Object.prototype.hasOwnProperty.call(data, key)) { - output += "-" + spaces + contentToDiffLine(key, entry[key]) + "\n"; + if (!testOutput || !Object.prototype.hasOwnProperty.call(testOutput, key)) { + output += "-" + spaces + contentToDiffLine(key, expected[key]) + "\n"; continue; } - const value = data[key]; - if (value !== entry[key]) { - output += "-" + spaces + contentToDiffLine(key, entry[key]) + "\n"; + const value = valueMapper(key, testOutput); + if (value !== expected[key]) { + output += "-" + spaces + contentToDiffLine(key, expected[key]) + "\n"; output += "+" + spaces + contentToDiffLine(key, value) + "\n"; } else { - output += spaces + contentToDiffLine(key, value) + "\n"; + output += spaces + " " + contentToDiffLine(key, value) + "\n"; } } return output + " }"; } -function lookForEntry(entry, data) { - return data.findIndex(data_entry => { +function lookForEntry(expected, testOutput) { + return testOutput.findIndex(testOutputEntry => { let allGood = true; - for (const key in entry) { - if (!Object.prototype.hasOwnProperty.call(entry, key)) { + for (const key in expected) { + if (!Object.prototype.hasOwnProperty.call(expected, key)) { continue; } - let value = data_entry[key]; - // To make our life easier, if there is a "parent" type, we add it to the path. - if (key === "path" && data_entry["parent"] !== undefined) { - if (value.length > 0) { - value += "::" + data_entry["parent"]["name"]; - } else { - value = data_entry["parent"]["name"]; - } - } - if (value !== entry[key]) { + const value = valueMapper(key, testOutputEntry); + if (value !== expected[key]) { allGood = false; break; } From c079c96877fdd3977c8d5be830931ecb4f79018d Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 15 Jul 2025 21:08:18 +0200 Subject: [PATCH 3/4] Add test for aliases partial match --- tests/rustdoc-js-std/alias-lev.js | 11 +++++++++++ tests/rustdoc-js/non-english-identifier.js | 17 +++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 tests/rustdoc-js-std/alias-lev.js diff --git a/tests/rustdoc-js-std/alias-lev.js b/tests/rustdoc-js-std/alias-lev.js new file mode 100644 index 0000000000000..17f3dc25d7633 --- /dev/null +++ b/tests/rustdoc-js-std/alias-lev.js @@ -0,0 +1,11 @@ +// This test ensures that aliases are also allowed to be partially matched. + +// ignore-order + +const EXPECTED = { + // The full alias name is `getcwd`. + 'query': 'getcw', + 'others': [ + { 'path': 'std::env', 'name': 'current_dir', 'alias': 'getcwd' }, + ], +}; diff --git a/tests/rustdoc-js/non-english-identifier.js b/tests/rustdoc-js/non-english-identifier.js index f2180b4c75530..3d50bd3ee9057 100644 --- a/tests/rustdoc-js/non-english-identifier.js +++ b/tests/rustdoc-js/non-english-identifier.js @@ -115,11 +115,10 @@ const EXPECTED = [ query: '加法', others: [ { - name: "add", + name: "加法", path: "non_english_identifier", - is_alias: true, - alias: "加法", - href: "../non_english_identifier/macro.add.html" + href: "../non_english_identifier/trait.加法.html", + desc: "Add" }, { name: "add", @@ -129,11 +128,13 @@ const EXPECTED = [ href: "../non_english_identifier/fn.add.html" }, { - name: "加法", + name: "add", path: "non_english_identifier", - href: "../non_english_identifier/trait.加法.html", - desc: "Add" - }], + is_alias: true, + alias: "加法", + href: "../non_english_identifier/macro.add.html" + }, + ], in_args: [{ name: "加上", path: "non_english_identifier::加法", From 2f14d0a65d7ec8ee35e7f25ddd996ae40615a2a3 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 19 Jul 2025 22:29:13 +0200 Subject: [PATCH 4/4] Add code comment explaining better what `Row.name` is for doc aliases --- src/librustdoc/html/static/js/rustdoc.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts index 9ce24d1c06f60..a958976454781 100644 --- a/src/librustdoc/html/static/js/rustdoc.d.ts +++ b/src/librustdoc/html/static/js/rustdoc.d.ts @@ -219,6 +219,8 @@ declare namespace rustdoc { crate: string, descShard: SearchDescShard, id: number, + // This is the name of the item. For doc aliases, if you want the name of the aliased + // item, take a look at `Row.original.name`. name: string, normalizedName: string, word: string,