Skip to content

Commit 7b92655

Browse files
committed
rustdoc-search: add search query syntax Fn(T) -> U
This is implemented, in addition to the ML-style one, because Rust does it. If we don't, we'll never hear the end of it. This commit also refactors some duplicate parts of the parser into a dedicated function.
1 parent 23e931f commit 7b92655

File tree

6 files changed

+530
-81
lines changed

6 files changed

+530
-81
lines changed

src/doc/rustdoc/src/read-documentation/search.md

+30-16
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,26 @@ will match these queries:
153153

154154
But it *does not* match `Result<Vec, u8>` or `Result<u8<Vec>>`.
155155

156+
To search for a function that accepts a function as a parameter,
157+
like `Iterator::all`, wrap the nested signature in parenthesis,
158+
as in [`Iterator<T>, (T -> bool) -> bool`][iterator-all].
159+
You can also search for a specific closure trait,
160+
such as `Iterator<T>, (FnMut(T) -> bool) -> bool`,
161+
but you need to know which one you want.
162+
163+
[iterator-all]: ../../std/vec/struct.Vec.html?search=Iterator<T>%2C+(T+->+bool)+->+bool&filter-crate=std
164+
156165
### Primitives with Special Syntax
157166

158-
| Shorthand | Explicit names |
159-
| --------- | ------------------------------------------------ |
160-
| `[]` | `primitive:slice` and/or `primitive:array` |
161-
| `[T]` | `primitive:slice<T>` and/or `primitive:array<T>` |
162-
| `()` | `primitive:unit` and/or `primitive:tuple` |
163-
| `(T)` | `T` |
164-
| `(T,)` | `primitive:tuple<T>` |
165-
| `!` | `primitive:never` |
167+
| Shorthand | Explicit names |
168+
| ---------------- | ------------------------------------------------- |
169+
| `[]` | `primitive:slice` and/or `primitive:array` |
170+
| `[T]` | `primitive:slice<T>` and/or `primitive:array<T>` |
171+
| `()` | `primitive:unit` and/or `primitive:tuple` |
172+
| `(T)` | `T` |
173+
| `(T,)` | `primitive:tuple<T>` |
174+
| `!` | `primitive:never` |
175+
| `(T, U -> V, W)` | `fn(T, U) -> (V, W)`, `Fn`, `FnMut`, and `FnOnce` |
166176

167177
When searching for `[]`, Rustdoc will return search results with either slices
168178
or arrays. If you know which one you want, you can force it to return results
@@ -182,6 +192,10 @@ results for types that match tuples, even though it also matches the type on
182192
its own. That is, `(u32)` matches `(u32,)` for the exact same reason that it
183193
also matches `Result<u32, Error>`.
184194

195+
The `->` operator has lower precedence than comma. If it's not wrapped
196+
in brackets, it delimits the return value for the function being searched for.
197+
To search for functions that take functions as parameters, use parenthesis.
198+
185199
### Limitations and quirks of type-based search
186200

187201
Type-based search is still a buggy, experimental, work-in-progress feature.
@@ -220,9 +234,6 @@ Most of these limitations should be addressed in future version of Rustdoc.
220234

221235
* Searching for lifetimes is not supported.
222236

223-
* It's impossible to search for closures based on their parameters or
224-
return values.
225-
226237
* It's impossible to search based on the length of an array.
227238

228239
## Item filtering
@@ -239,19 +250,21 @@ Item filters can be used in both name-based and type signature-based searches.
239250

240251
```text
241252
ident = *(ALPHA / DIGIT / "_")
242-
path = ident *(DOUBLE-COLON ident) [!]
253+
path = ident *(DOUBLE-COLON ident) [BANG]
243254
slice-like = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET
244255
tuple-like = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN
245-
arg = [type-filter *WS COLON *WS] (path [generics] / slice-like / tuple-like / [!])
256+
arg = [type-filter *WS COLON *WS] (path [generics] / slice-like / tuple-like)
246257
type-sep = COMMA/WS *(COMMA/WS)
247-
nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep)
258+
nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) [ return-args ]
248259
generic-arg-list = *(type-sep) arg [ EQUAL arg ] *(type-sep arg [ EQUAL arg ]) *(type-sep)
249-
generics = OPEN-ANGLE-BRACKET [ generic-arg-list ] *(type-sep)
260+
normal-generics = OPEN-ANGLE-BRACKET [ generic-arg-list ] *(type-sep)
250261
CLOSE-ANGLE-BRACKET
262+
fn-like-generics = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN [ RETURN-ARROW arg ]
263+
generics = normal-generics / fn-like-generics
251264
return-args = RETURN-ARROW *(type-sep) nonempty-arg-list
252265
253266
exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ]
254-
type-search = [ nonempty-arg-list ] [ return-args ]
267+
type-search = [ nonempty-arg-list ]
255268
256269
query = *WS (exact-search / type-search) *WS
257270
@@ -296,6 +309,7 @@ QUOTE = %x22
296309
COMMA = ","
297310
RETURN-ARROW = "->"
298311
EQUAL = "="
312+
BANG = "!"
299313
300314
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
301315
DIGIT = %x30-39

src/librustdoc/html/static/js/search.js

+65-48
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// ignore-tidy-filelength
12
/* global addClass, getNakedUrl, getSettingValue */
23
/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */
34

@@ -578,7 +579,10 @@ function initSearch(rawSearchIndex) {
578579
// Syntactically, bindings are parsed as generics,
579580
// but the query engine treats them differently.
580581
if (gen.bindingName !== null) {
581-
bindings.set(gen.bindingName.name, [gen, ...gen.bindingName.generics]);
582+
if (gen.name !== null) {
583+
gen.bindingName.generics.unshift(gen);
584+
}
585+
bindings.set(gen.bindingName.name, gen.bindingName.generics);
582586
return false;
583587
}
584588
return true;
@@ -678,6 +682,38 @@ function initSearch(rawSearchIndex) {
678682
return end;
679683
}
680684

685+
function getFilteredNextElem(query, parserState, elems, isInGenerics) {
686+
const start = parserState.pos;
687+
if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) {
688+
throw ["Expected type filter before ", ":"];
689+
}
690+
getNextElem(query, parserState, elems, isInGenerics);
691+
if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) {
692+
if (parserState.typeFilter !== null) {
693+
throw [
694+
"Unexpected ",
695+
":",
696+
" (expected path after type filter ",
697+
parserState.typeFilter + ":",
698+
")",
699+
];
700+
}
701+
if (elems.length === 0) {
702+
throw ["Expected type filter before ", ":"];
703+
} else if (query.literalSearch) {
704+
throw ["Cannot use quotes on type filter"];
705+
}
706+
// The type filter doesn't count as an element since it's a modifier.
707+
const typeFilterElem = elems.pop();
708+
checkExtraTypeFilterCharacters(start, parserState);
709+
parserState.typeFilter = typeFilterElem.name;
710+
parserState.pos += 1;
711+
parserState.totalElems -= 1;
712+
query.literalSearch = false;
713+
getNextElem(query, parserState, elems, isInGenerics);
714+
}
715+
}
716+
681717
/**
682718
* @param {ParsedQuery} query
683719
* @param {ParserState} parserState
@@ -752,6 +788,32 @@ function initSearch(rawSearchIndex) {
752788
}
753789
parserState.pos += 1;
754790
getItemsBefore(query, parserState, generics, ">");
791+
} else if (parserState.pos < parserState.length &&
792+
parserState.userQuery[parserState.pos] === "("
793+
) {
794+
if (start >= end) {
795+
throw ["Found generics without a path"];
796+
}
797+
if (parserState.isInBinding) {
798+
throw ["Unexpected ", "(", " after ", "="];
799+
}
800+
parserState.pos += 1;
801+
const typeFilter = parserState.typeFilter;
802+
parserState.typeFilter = null;
803+
getItemsBefore(query, parserState, generics, ")");
804+
skipWhitespace(parserState);
805+
if (isReturnArrow(parserState)) {
806+
parserState.pos += 2;
807+
skipWhitespace(parserState);
808+
getFilteredNextElem(query, parserState, generics, isInGenerics);
809+
generics[generics.length - 1].bindingName = makePrimitiveElement("output");
810+
} else {
811+
generics.push(makePrimitiveElement(null, {
812+
bindingName: makePrimitiveElement("output"),
813+
typeFilter: null,
814+
}));
815+
}
816+
parserState.typeFilter = typeFilter;
755817
}
756818
if (isStringElem) {
757819
skipWhitespace(parserState);
@@ -811,7 +873,6 @@ function initSearch(rawSearchIndex) {
811873
function getItemsBefore(query, parserState, elems, endChar) {
812874
let foundStopChar = true;
813875
let foundSeparator = false;
814-
let start = parserState.pos;
815876

816877
// If this is a generic, keep the outer item's type filter around.
817878
const oldTypeFilter = parserState.typeFilter;
@@ -874,24 +935,6 @@ function initSearch(rawSearchIndex) {
874935
continue;
875936
} else if (c === ":" && isPathStart(parserState)) {
876937
throw ["Unexpected ", "::", ": paths cannot start with ", "::"];
877-
} else if (c === ":") {
878-
if (parserState.typeFilter !== null) {
879-
throw ["Unexpected ", ":"];
880-
}
881-
if (elems.length === 0) {
882-
throw ["Expected type filter before ", ":"];
883-
} else if (query.literalSearch) {
884-
throw ["Cannot use quotes on type filter"];
885-
}
886-
// The type filter doesn't count as an element since it's a modifier.
887-
const typeFilterElem = elems.pop();
888-
checkExtraTypeFilterCharacters(start, parserState);
889-
parserState.typeFilter = typeFilterElem.name;
890-
parserState.pos += 1;
891-
parserState.totalElems -= 1;
892-
query.literalSearch = false;
893-
foundStopChar = true;
894-
continue;
895938
} else if (isEndCharacter(c)) {
896939
throw ["Unexpected ", c, " after ", extra];
897940
}
@@ -926,8 +969,7 @@ function initSearch(rawSearchIndex) {
926969
];
927970
}
928971
const posBefore = parserState.pos;
929-
start = parserState.pos;
930-
getNextElem(query, parserState, elems, endChar !== "");
972+
getFilteredNextElem(query, parserState, elems, endChar !== "");
931973
if (endChar !== "" && parserState.pos >= parserState.length) {
932974
throw ["Unclosed ", extra];
933975
}
@@ -1004,7 +1046,6 @@ function initSearch(rawSearchIndex) {
10041046
*/
10051047
function parseInput(query, parserState) {
10061048
let foundStopChar = true;
1007-
let start = parserState.pos;
10081049

10091050
while (parserState.pos < parserState.length) {
10101051
const c = parserState.userQuery[parserState.pos];
@@ -1022,29 +1063,6 @@ function initSearch(rawSearchIndex) {
10221063
throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]];
10231064
}
10241065
throw ["Unexpected ", c];
1025-
} else if (c === ":" && !isPathStart(parserState)) {
1026-
if (parserState.typeFilter !== null) {
1027-
throw [
1028-
"Unexpected ",
1029-
":",
1030-
" (expected path after type filter ",
1031-
parserState.typeFilter + ":",
1032-
")",
1033-
];
1034-
} else if (query.elems.length === 0) {
1035-
throw ["Expected type filter before ", ":"];
1036-
} else if (query.literalSearch) {
1037-
throw ["Cannot use quotes on type filter"];
1038-
}
1039-
// The type filter doesn't count as an element since it's a modifier.
1040-
const typeFilterElem = query.elems.pop();
1041-
checkExtraTypeFilterCharacters(start, parserState);
1042-
parserState.typeFilter = typeFilterElem.name;
1043-
parserState.pos += 1;
1044-
parserState.totalElems -= 1;
1045-
query.literalSearch = false;
1046-
foundStopChar = true;
1047-
continue;
10481066
} else if (c === " ") {
10491067
skipWhitespace(parserState);
10501068
continue;
@@ -1080,8 +1098,7 @@ function initSearch(rawSearchIndex) {
10801098
];
10811099
}
10821100
const before = query.elems.length;
1083-
start = parserState.pos;
1084-
getNextElem(query, parserState, query.elems, false);
1101+
getFilteredNextElem(query, parserState, query.elems, false);
10851102
if (query.elems.length === before) {
10861103
// Nothing was added, weird... Let's increase the position to not remain stuck.
10871104
parserState.pos += 1;

tests/rustdoc-js-std/parser-errors.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ const PARSED = [
195195
original: "a (b:",
196196
returned: [],
197197
userQuery: "a (b:",
198-
error: "Expected `,`, `:` or `->`, found `(`",
198+
error: "Unclosed `(`",
199199
},
200200
{
201201
query: "_:",
@@ -357,7 +357,16 @@ const PARSED = [
357357
original: "a,:",
358358
returned: [],
359359
userQuery: "a,:",
360-
error: 'Unexpected `,` in type filter (before `:`)',
360+
error: 'Expected type filter before `:`',
361+
},
362+
{
363+
query: "a!:",
364+
elems: [],
365+
foundElems: 0,
366+
original: "a!:",
367+
returned: [],
368+
userQuery: "a!:",
369+
error: 'Unexpected `!` in type filter (before `:`)',
361370
},
362371
{
363372
query: " a<> :",
@@ -366,7 +375,7 @@ const PARSED = [
366375
original: "a<> :",
367376
returned: [],
368377
userQuery: "a<> :",
369-
error: 'Unexpected `<` in type filter (before `:`)',
378+
error: 'Expected `,`, `:` or `->` after `>`, found `:`',
370379
},
371380
{
372381
query: "mod : :",

0 commit comments

Comments
 (0)