about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Howell <michael@notriddle.com>2023-09-29 12:52:30 -0700
committerMichael Howell <michael@notriddle.com>2023-12-26 12:54:17 -0700
commitf6a045cc6b24dd033c207dd63d8adccdedf672d2 (patch)
tree7ccbf87bff8e0dc808f45d9d9393cbab4a093be3
parenta75fed74b62f95d1659ff70bea7895ed5c85bdba (diff)
downloadrust-f6a045cc6b24dd033c207dd63d8adccdedf672d2.tar.gz
rust-f6a045cc6b24dd033c207dd63d8adccdedf672d2.zip
rustdoc: search for tuples and unit by type with `()`
-rw-r--r--src/doc/rustdoc/src/read-documentation/search.md67
-rw-r--r--src/librustdoc/html/render/search_index.rs3
-rw-r--r--src/librustdoc/html/static/js/search.js100
-rw-r--r--tests/rustdoc-js-std/parser-errors.js17
-rw-r--r--tests/rustdoc-js-std/parser-slice-array.js18
-rw-r--r--tests/rustdoc-js-std/parser-tuple.js365
-rw-r--r--tests/rustdoc-js-std/parser-weird-queries.js2
-rw-r--r--tests/rustdoc-js/tuple-unit.js80
-rw-r--r--tests/rustdoc-js/tuple-unit.rs18
9 files changed, 616 insertions, 54 deletions
diff --git a/src/doc/rustdoc/src/read-documentation/search.md b/src/doc/rustdoc/src/read-documentation/search.md
index 1f45bd6c6b8..d773794504e 100644
--- a/src/doc/rustdoc/src/read-documentation/search.md
+++ b/src/doc/rustdoc/src/read-documentation/search.md
@@ -150,12 +150,55 @@ will match these queries:
 
 But it *does not* match `Result<Vec, u8>` or `Result<u8<Vec>>`.
 
-Function signature searches also support arrays and slices. The explicit name
-`primitive:slice<u8>` and `primitive:array<u8>` can be used to match a slice
-or array of bytes, while square brackets `[u8]` will match either one. Empty
-square brackets, `[]`, will match any slice or array regardless of what
-it contains, while a slice with a type parameter, like `[T]`, will only match
-functions that actually operate on generic slices.
+### Primitives with Special Syntax
+
+<table>
+<thead>
+  <tr>
+    <th>Shorthand</th>
+    <th>Explicit names</th>
+  </tr>
+</thead>
+<tbody>
+  <tr>
+    <td><code>[]</code></td>
+    <td><code>primitive:slice</code> and/or <code>primitive:array</code></td>
+  </tr>
+  <tr>
+    <td><code>[T]</code></td>
+    <td><code>primitive:slice&lt;T&gt;</code> and/or <code>primitive:array&lt;T&gt;</code></td>
+  </tr>
+  <tr>
+    <td><code>()</code></td>
+    <td><code>primitive:unit</code> and/or <code>primitive:tuple</code></td>
+  </tr>
+  <tr>
+    <td><code>(T)</code></td>
+    <td><code>T</code></td>
+  </tr>
+  <tr>
+    <td><code>(T,)</code></td>
+    <td><code>primitive:tuple&lt;T&gt;</code></td>
+  </tr>
+  <tr>
+    <td><code>!</code></td>
+    <td><code>primitive:never</code></td>
+  </tr>
+</tbody>
+</table>
+
+When searching for `[]`, Rustdoc will return search results with either slices
+or arrays. If you know which one you want, you can force it to return results
+for `primitive:slice` or `primitive:array` using the explicit name syntax.
+Empty square brackets, `[]`, will match any slice or array regardless of what
+it contains, or an item type can be provided, such as `[u8]` or `[T]`, to
+explicitly find functions that operate on byte slices or generic slices,
+respectively.
+
+A single type expression wrapped in parens is the same as that type expression,
+since parens act as the grouping operator. If they're empty, though, they will
+match both `unit` and `tuple`, and if there's more than one type (or a trailing
+or leading comma) it is the same as `primitive:tuple<...>`.
 
 ### Limitations and quirks of type-based search
 
@@ -188,11 +231,10 @@ Most of these limitations should be addressed in future version of Rustdoc.
     that you don't want a type parameter, you can force it to match
     something else by giving it a different prefix like `struct:T`.
 
-  * It's impossible to search for references, pointers, or tuples. The
+  * It's impossible to search for references or pointers. The
     wrapped types can be searched for, so a function that takes `&File` can
     be found with `File`, but you'll get a parse error when typing an `&`
-    into the search field. Similarly, `Option<(T, U)>` can be matched with
-    `Option<T, U>`, but `(` will give a parse error.
+    into the search field.
 
   * Searching for lifetimes is not supported.
 
@@ -216,8 +258,9 @@ Item filters can be used in both name-based and type signature-based searches.
 ```text
 ident = *(ALPHA / DIGIT / "_")
 path = ident *(DOUBLE-COLON ident) [!]
-slice = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET
-arg = [type-filter *WS COLON *WS] (path [generics] / slice / [!])
+slice-like = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET
+tuple-like = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN
+arg = [type-filter *WS COLON *WS] (path [generics] / slice-like / tuple-like / [!])
 type-sep = COMMA/WS *(COMMA/WS)
 nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep)
 generic-arg-list = *(type-sep) arg [ EQUAL arg ] *(type-sep arg [ EQUAL arg ]) *(type-sep)
@@ -263,6 +306,8 @@ OPEN-ANGLE-BRACKET = "<"
 CLOSE-ANGLE-BRACKET = ">"
 OPEN-SQUARE-BRACKET = "["
 CLOSE-SQUARE-BRACKET = "]"
+OPEN-PAREN = "("
+CLOSE-PAREN = ")"
 COLON = ":"
 DOUBLE-COLON = "::"
 QUOTE = %x22
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index a1029320d2d..6c6a31bbb11 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -581,6 +581,9 @@ fn get_index_type_id(
         // The type parameters are converted to generics in `simplify_fn_type`
         clean::Slice(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Slice)),
         clean::Array(_, _) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Array)),
+        clean::Tuple(ref n) if n.is_empty() => {
+            Some(RenderTypeId::Primitive(clean::PrimitiveType::Unit))
+        }
         clean::Tuple(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Tuple)),
         clean::QPath(ref data) => {
             if data.self_type.is_self_type()
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index e824a1fd4bd..b2263e9e3e1 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -260,6 +260,18 @@ function initSearch(rawSearchIndex) {
      * Special type name IDs for searching by both array and slice (`[]` syntax).
      */
     let typeNameIdOfArrayOrSlice;
+    /**
+     * Special type name IDs for searching by tuple.
+     */
+    let typeNameIdOfTuple;
+    /**
+     * Special type name IDs for searching by unit.
+     */
+    let typeNameIdOfUnit;
+    /**
+     * Special type name IDs for searching by both tuple and unit (`()` syntax).
+     */
+    let typeNameIdOfTupleOrUnit;
 
     /**
      * Add an item to the type Name->ID map, or, if one already exists, use it.
@@ -295,11 +307,7 @@ function initSearch(rawSearchIndex) {
     }
 
     function isEndCharacter(c) {
-        return "=,>-]".indexOf(c) !== -1;
-    }
-
-    function isErrorCharacter(c) {
-        return "()".indexOf(c) !== -1;
+        return "=,>-])".indexOf(c) !== -1;
     }
 
     function itemTypeFromName(typename) {
@@ -585,8 +593,6 @@ function initSearch(rawSearchIndex) {
                         throw ["Unexpected ", "!", ": it can only be at the end of an ident"];
                     }
                     foundExclamation = parserState.pos;
-                } else if (isErrorCharacter(c)) {
-                    throw ["Unexpected ", c];
                 } else if (isPathSeparator(c)) {
                     if (c === ":") {
                         if (!isPathStart(parserState)) {
@@ -616,11 +622,14 @@ function initSearch(rawSearchIndex) {
                     }
                 } else if (
                     c === "[" ||
+                    c === "(" ||
                     isEndCharacter(c) ||
                     isSpecialStartCharacter(c) ||
                     isSeparatorCharacter(c)
                 ) {
                     break;
+                } else if (parserState.pos > 0) {
+                    throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]];
                 } else {
                     throw ["Unexpected ", c];
                 }
@@ -661,15 +670,24 @@ function initSearch(rawSearchIndex) {
         skipWhitespace(parserState);
         let start = parserState.pos;
         let end;
-        if (parserState.userQuery[parserState.pos] === "[") {
+        if ("[(".indexOf(parserState.userQuery[parserState.pos]) !== -1) {
+let endChar = ")";
+let name = "()";
+let friendlyName = "tuple";
+
+if (parserState.userQuery[parserState.pos] === "[") {
+    endChar = "]";
+    name = "[]";
+    friendlyName = "slice";
+}
             parserState.pos += 1;
-            getItemsBefore(query, parserState, generics, "]");
+            const { foundSeparator } = getItemsBefore(query, parserState, generics, endChar);
             const typeFilter = parserState.typeFilter;
             const isInBinding = parserState.isInBinding;
             if (typeFilter !== null && typeFilter !== "primitive") {
                 throw [
                     "Invalid search type: primitive ",
-                    "[]",
+                    name,
                     " and ",
                     typeFilter,
                     " both specified",
@@ -677,27 +695,31 @@ function initSearch(rawSearchIndex) {
             }
             parserState.typeFilter = null;
             parserState.isInBinding = null;
-            parserState.totalElems += 1;
-            if (isInGenerics) {
-                parserState.genericsElems += 1;
-            }
             for (const gen of generics) {
                 if (gen.bindingName !== null) {
-                    throw ["Type parameter ", "=", " cannot be within slice ", "[]"];
+                    throw ["Type parameter ", "=", ` cannot be within ${friendlyName} `, name];
                 }
             }
-            elems.push({
-                name: "[]",
-                id: null,
-                fullPath: ["[]"],
-                pathWithoutLast: [],
-                pathLast: "[]",
-                normalizedPathLast: "[]",
-                generics,
-                typeFilter: "primitive",
-                bindingName: isInBinding,
-                bindings: new Map(),
-            });
+            if (name === "()" && !foundSeparator && generics.length === 1 && typeFilter === null) {
+                elems.push(generics[0]);
+            } else {
+                parserState.totalElems += 1;
+                if (isInGenerics) {
+                    parserState.genericsElems += 1;
+                }
+                elems.push({
+                    name: name,
+                    id: null,
+                    fullPath: [name],
+                    pathWithoutLast: [],
+                    pathLast: name,
+                    normalizedPathLast: name,
+                    generics,
+                    bindings: new Map(),
+                    typeFilter: "primitive",
+                    bindingName: isInBinding,
+                });
+            }
         } else {
             const isStringElem = parserState.userQuery[start] === "\"";
             // We handle the strings on their own mostly to make code easier to follow.
@@ -770,9 +792,11 @@ function initSearch(rawSearchIndex) {
      * @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.
+     * @returns {{foundSeparator: bool}}
      */
     function getItemsBefore(query, parserState, elems, endChar) {
         let foundStopChar = true;
+        let foundSeparator = false;
         let start = parserState.pos;
 
         // If this is a generic, keep the outer item's type filter around.
@@ -786,6 +810,8 @@ function initSearch(rawSearchIndex) {
             extra = "<";
         } else if (endChar === "]") {
             extra = "[";
+        } else if (endChar === ")") {
+            extra = "(";
         } else if (endChar === "") {
             extra = "->";
         } else {
@@ -802,6 +828,7 @@ function initSearch(rawSearchIndex) {
             } else if (isSeparatorCharacter(c)) {
                 parserState.pos += 1;
                 foundStopChar = true;
+                foundSeparator = true;
                 continue;
             } else if (c === ":" && isPathStart(parserState)) {
                 throw ["Unexpected ", "::", ": paths cannot start with ", "::"];
@@ -879,6 +906,8 @@ function initSearch(rawSearchIndex) {
 
         parserState.typeFilter = oldTypeFilter;
         parserState.isInBinding = oldIsInBinding;
+
+        return { foundSeparator };
     }
 
     /**
@@ -926,6 +955,8 @@ function initSearch(rawSearchIndex) {
                         break;
                     }
                     throw ["Unexpected ", c, " (did you mean ", "->", "?)"];
+                } else if (parserState.pos > 0) {
+                    throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]];
                 }
                 throw ["Unexpected ", c];
             } else if (c === ":" && !isPathStart(parserState)) {
@@ -1599,6 +1630,11 @@ function initSearch(rawSearchIndex) {
                 ) {
                     // [] matches primitive:array or primitive:slice
                     // if it matches, then we're fine, and this is an appropriate match candidate
+                } else if (queryElem.id === typeNameIdOfTupleOrUnit &&
+                    (fnType.id === typeNameIdOfTuple || fnType.id === typeNameIdOfUnit)
+                ) {
+                    // () matches primitive:tuple or primitive:unit
+                    // if it matches, then we're fine, and this is an appropriate match candidate
                 } else if (fnType.id !== queryElem.id || queryElem.id === null) {
                     return false;
                 }
@@ -1792,7 +1828,7 @@ function initSearch(rawSearchIndex) {
                 if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 &&
                     typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 &&
                     // special case
-                    elem.id !== typeNameIdOfArrayOrSlice
+                    elem.id !== typeNameIdOfArrayOrSlice && elem.id !== typeNameIdOfTupleOrUnit
                 ) {
                     return row.id === elem.id || checkIfInList(
                         row.generics,
@@ -2822,12 +2858,15 @@ ${item.displayPath}<span class="${type}">${name}</span>\
      */
     function buildFunctionTypeFingerprint(type, output, fps) {
         let input = type.id;
-        // All forms of `[]` get collapsed down to one thing in the bloom filter.
+        // All forms of `[]`/`()` get collapsed down to one thing in the bloom filter.
         // Differentiating between arrays and slices, if the user asks for it, is
         // still done in the matching algorithm.
         if (input === typeNameIdOfArray || input === typeNameIdOfSlice) {
             input = typeNameIdOfArrayOrSlice;
         }
+        if (input === typeNameIdOfTuple || input === typeNameIdOfUnit) {
+            input = typeNameIdOfTupleOrUnit;
+        }
         // http://burtleburtle.net/bob/hash/integer.html
         // ~~ is toInt32. It's used before adding, so
         // the number stays in safe integer range.
@@ -2922,7 +2961,10 @@ ${item.displayPath}<span class="${type}">${name}</span>\
         // that can be searched using `[]` syntax.
         typeNameIdOfArray = buildTypeMapIndex("array");
         typeNameIdOfSlice = buildTypeMapIndex("slice");
+        typeNameIdOfTuple = buildTypeMapIndex("tuple");
+        typeNameIdOfUnit = buildTypeMapIndex("unit");
         typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]");
+        typeNameIdOfTupleOrUnit = buildTypeMapIndex("()");
 
         // Function type fingerprints are 128-bit bloom filters that are used to
         // estimate the distance between function and query.
diff --git a/tests/rustdoc-js-std/parser-errors.js b/tests/rustdoc-js-std/parser-errors.js
index f9f9c4f4de8..16d171260da 100644
--- a/tests/rustdoc-js-std/parser-errors.js
+++ b/tests/rustdoc-js-std/parser-errors.js
@@ -24,7 +24,7 @@ const PARSED = [
         original: "-> *",
         returned: [],
         userQuery: "-> *",
-        error: "Unexpected `*`",
+        error: "Unexpected `*` after ` `",
     },
     {
         query: 'a<"P">',
@@ -108,22 +108,13 @@ const PARSED = [
         error: "Unexpected `::`: paths cannot start with `::`",
     },
     {
-        query: "((a))",
-        elems: [],
-        foundElems: 0,
-        original: "((a))",
-        returned: [],
-        userQuery: "((a))",
-        error: "Unexpected `(`",
-    },
-    {
         query: "(p -> p",
         elems: [],
         foundElems: 0,
         original: "(p -> p",
         returned: [],
         userQuery: "(p -> p",
-        error: "Unexpected `(`",
+        error: "Unexpected `-` after `(`",
     },
     {
         query: "::a::b",
@@ -204,7 +195,7 @@ const PARSED = [
         original: "a (b:",
         returned: [],
         userQuery: "a (b:",
-        error: "Unexpected `(`",
+        error: "Expected `,`, `:` or `->`, found `(`",
     },
     {
         query: "_:",
@@ -249,7 +240,7 @@ const PARSED = [
         original: "ab'",
         returned: [],
         userQuery: "ab'",
-        error: "Unexpected `'`",
+        error: "Unexpected `'` after `b`",
     },
     {
         query: "a->",
diff --git a/tests/rustdoc-js-std/parser-slice-array.js b/tests/rustdoc-js-std/parser-slice-array.js
index 239391bed42..1de52af94e6 100644
--- a/tests/rustdoc-js-std/parser-slice-array.js
+++ b/tests/rustdoc-js-std/parser-slice-array.js
@@ -267,6 +267,24 @@ const PARSED = [
         error: "Unexpected `]`",
     },
     {
+        query: '[a<b>',
+        elems: [],
+        foundElems: 0,
+        original: "[a<b>",
+        returned: [],
+        userQuery: "[a<b>",
+        error: "Unclosed `[`",
+    },
+    {
+        query: 'a<b>]',
+        elems: [],
+        foundElems: 0,
+        original: "a<b>]",
+        returned: [],
+        userQuery: "a<b>]",
+        error: "Unexpected `]` after `>`",
+    },
+    {
         query: 'primitive:[u8]',
         elems: [
             {
diff --git a/tests/rustdoc-js-std/parser-tuple.js b/tests/rustdoc-js-std/parser-tuple.js
new file mode 100644
index 00000000000..eb16289d3c0
--- /dev/null
+++ b/tests/rustdoc-js-std/parser-tuple.js
@@ -0,0 +1,365 @@
+const PARSED = [
+    {
+        query: '(((D, ()))',
+        elems: [],
+        foundElems: 0,
+        original: '(((D, ()))',
+        returned: [],
+        userQuery: '(((d, ()))',
+        error: 'Unclosed `(`',
+    },
+    {
+        query: '(((D, ())))',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [
+                    {
+                        name: "d",
+                        fullPath: ["d"],
+                        pathWithoutLast: [],
+                        pathLast: "d",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                    {
+                        name: "()",
+                        fullPath: ["()"],
+                        pathWithoutLast: [],
+                        pathLast: "()",
+                        generics: [],
+                        typeFilter: 1,
+                    },
+                ],
+                typeFilter: 1,
+            }
+        ],
+        foundElems: 1,
+        original: '(((D, ())))',
+        returned: [],
+        userQuery: '(((d, ())))',
+        error: null,
+    },
+    {
+        query: '(),u8',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [],
+                typeFilter: 1,
+            },
+            {
+                name: "u8",
+                fullPath: ["u8"],
+                pathWithoutLast: [],
+                pathLast: "u8",
+                generics: [],
+                typeFilter: -1,
+            },
+        ],
+        foundElems: 2,
+        original: "(),u8",
+        returned: [],
+        userQuery: "(),u8",
+        error: null,
+    },
+    // Parens act as grouping operators when:
+    // - there's no commas directly nested within
+    // - there's at least two child types (zero means unit)
+    // - it's not tagged with a type filter
+    // Otherwise, they represent unit and/or tuple. To search for
+    // unit or tuple specifically, use `primitive:unit` or `primitive:tuple<...>`.
+    {
+        query: '(u8)',
+        elems: [
+            {
+                name: "u8",
+                fullPath: ["u8"],
+                pathWithoutLast: [],
+                pathLast: "u8",
+                generics: [],
+                typeFilter: -1,
+            },
+        ],
+        foundElems: 1,
+        original: "(u8)",
+        returned: [],
+        userQuery: "(u8)",
+        error: null,
+    },
+    {
+        query: '(u8,)',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                ],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "(u8,)",
+        returned: [],
+        userQuery: "(u8,)",
+        error: null,
+    },
+    {
+        query: '(,u8)',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                ],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "(,u8)",
+        returned: [],
+        userQuery: "(,u8)",
+        error: null,
+    },
+    {
+        query: 'primitive:(u8)',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                ],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "primitive:(u8)",
+        returned: [],
+        userQuery: "primitive:(u8)",
+        error: null,
+    },
+    {
+        query: '(primitive:u8)',
+        elems: [
+            {
+                name: "u8",
+                fullPath: ["u8"],
+                pathWithoutLast: [],
+                pathLast: "u8",
+                generics: [],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "(primitive:u8)",
+        returned: [],
+        userQuery: "(primitive:u8)",
+        error: null,
+    },
+    {
+        query: '(u8,u8)',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                ],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "(u8,u8)",
+        returned: [],
+        userQuery: "(u8,u8)",
+        error: null,
+    },
+    {
+        query: '(u8<u8>)',
+        elems: [
+            {
+                name: "u8",
+                fullPath: ["u8"],
+                pathWithoutLast: [],
+                pathLast: "u8",
+                generics: [
+                    {
+                        name: "u8",
+                        fullPath: ["u8"],
+                        pathWithoutLast: [],
+                        pathLast: "u8",
+                        generics: [],
+                        typeFilter: -1,
+                    },
+                ],
+                typeFilter: -1,
+            },
+        ],
+        foundElems: 1,
+        original: "(u8<u8>)",
+        returned: [],
+        userQuery: "(u8<u8>)",
+        error: null,
+    },
+    {
+        query: '()',
+        elems: [
+            {
+                name: "()",
+                fullPath: ["()"],
+                pathWithoutLast: [],
+                pathLast: "()",
+                generics: [],
+                typeFilter: 1,
+            },
+        ],
+        foundElems: 1,
+        original: "()",
+        returned: [],
+        userQuery: "()",
+        error: null,
+    },
+    {
+        query: '(>',
+        elems: [],
+        foundElems: 0,
+        original: "(>",
+        returned: [],
+        userQuery: "(>",
+        error: "Unexpected `>` after `(`",
+    },
+    {
+        query: '(<',
+        elems: [],
+        foundElems: 0,
+        original: "(<",
+        returned: [],
+        userQuery: "(<",
+        error: "Found generics without a path",
+    },
+    {
+        query: '(a>',
+        elems: [],
+        foundElems: 0,
+        original: "(a>",
+        returned: [],
+        userQuery: "(a>",
+        error: "Unexpected `>` after `(`",
+    },
+    {
+        query: '(a<',
+        elems: [],
+        foundElems: 0,
+        original: "(a<",
+        returned: [],
+        userQuery: "(a<",
+        error: "Unclosed `<`",
+    },
+    {
+        query: '(a',
+        elems: [],
+        foundElems: 0,
+        original: "(a",
+        returned: [],
+        userQuery: "(a",
+        error: "Unclosed `(`",
+    },
+    {
+        query: '(',
+        elems: [],
+        foundElems: 0,
+        original: "(",
+        returned: [],
+        userQuery: "(",
+        error: "Unclosed `(`",
+    },
+    {
+        query: ')',
+        elems: [],
+        foundElems: 0,
+        original: ")",
+        returned: [],
+        userQuery: ")",
+        error: "Unexpected `)`",
+    },
+    {
+        query: '(a<b>',
+        elems: [],
+        foundElems: 0,
+        original: "(a<b>",
+        returned: [],
+        userQuery: "(a<b>",
+        error: "Unclosed `(`",
+    },
+    {
+        query: 'a<b>)',
+        elems: [],
+        foundElems: 0,
+        original: "a<b>)",
+        returned: [],
+        userQuery: "a<b>)",
+        error: "Unexpected `)` after `>`",
+    },
+    {
+        query: 'macro:(u8)',
+        elems: [],
+        foundElems: 0,
+        original: "macro:(u8)",
+        returned: [],
+        userQuery: "macro:(u8)",
+        error: "Invalid search type: primitive `()` and `macro` both specified",
+    },
+];
diff --git a/tests/rustdoc-js-std/parser-weird-queries.js b/tests/rustdoc-js-std/parser-weird-queries.js
index ba68c9717c5..26b8c32d680 100644
--- a/tests/rustdoc-js-std/parser-weird-queries.js
+++ b/tests/rustdoc-js-std/parser-weird-queries.js
@@ -44,7 +44,7 @@ const PARSED = [
         original: "a,b(c)",
         returned: [],
         userQuery: "a,b(c)",
-        error: "Unexpected `(`",
+        error: "Expected `,`, `:` or `->`, found `(`",
     },
     {
         query: 'aaa,a',
diff --git a/tests/rustdoc-js/tuple-unit.js b/tests/rustdoc-js/tuple-unit.js
new file mode 100644
index 00000000000..d24a3da328c
--- /dev/null
+++ b/tests/rustdoc-js/tuple-unit.js
@@ -0,0 +1,80 @@
+// exact-check
+
+const EXPECTED = [
+    {
+        'query': '()',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'side_effect' },
+            { 'path': 'tuple_unit', 'name': 'one' },
+            { 'path': 'tuple_unit', 'name': 'two' },
+            { 'path': 'tuple_unit', 'name': 'nest' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': 'primitive:unit',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'side_effect' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': 'primitive:tuple',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'one' },
+            { 'path': 'tuple_unit', 'name': 'two' },
+            { 'path': 'tuple_unit', 'name': 'nest' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(P)',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'not_tuple' },
+            { 'path': 'tuple_unit', 'name': 'one' },
+            { 'path': 'tuple_unit', 'name': 'two' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(P,)',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'one' },
+            { 'path': 'tuple_unit', 'name': 'two' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(P, P)',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'two' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(P, ())',
+        'returned': [],
+        'in_args': [],
+    },
+    {
+        'query': '(Q, ())',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'nest' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(R)',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'nest' },
+        ],
+        'in_args': [],
+    },
+    {
+        'query': '(u32)',
+        'returned': [
+            { 'path': 'tuple_unit', 'name': 'nest' },
+        ],
+        'in_args': [],
+    },
+];
diff --git a/tests/rustdoc-js/tuple-unit.rs b/tests/rustdoc-js/tuple-unit.rs
new file mode 100644
index 00000000000..93f9a671cbc
--- /dev/null
+++ b/tests/rustdoc-js/tuple-unit.rs
@@ -0,0 +1,18 @@
+pub struct P;
+pub struct Q;
+pub struct R<T>(T);
+
+// Checks that tuple and unit both work
+pub fn side_effect() { }
+
+// Check a non-tuple
+pub fn not_tuple() -> P { loop {} }
+
+// Check a 1-tuple
+pub fn one() -> (P,) { loop {} }
+
+// Check a 2-tuple
+pub fn two() -> (P,P) { loop {} }
+
+// Check a nested tuple
+pub fn nest() -> (Q, R<(u32,)>) { loop {} }