Make query parser more strict and improve display of errors

This commit is contained in:
Guillaume Gomez 2022-01-09 15:12:46 +01:00
parent 264064df36
commit 99d552092c
7 changed files with 243 additions and 142 deletions

View File

@ -132,17 +132,12 @@ window.initSearch = function(rawSearchIndex) {
return "(<\"".indexOf(c) !== -1;
}
function isStopCharacter(c) {
return isWhitespace(c) || "),>-=".indexOf(c) !== -1;
function isEndCharacter(c) {
return "),>-".indexOf(c) !== -1;
}
function removeEmptyStringsFromArray(arr) {
for (var i = 0, len = arr.length; i < len; ++i) {
if (arr[i] === "") {
arr.splice(i, 1);
i -= 1;
}
}
function isStopCharacter(c) {
return isWhitespace(c) || isEndCharacter(c);
}
function itemTypeFromName(typename) {
@ -151,7 +146,8 @@ window.initSearch = function(rawSearchIndex) {
return i;
}
}
return NO_TYPE_FILTER;
throw new Error("Unknown type filter `" + typename + "`");
}
/**
@ -189,22 +185,6 @@ window.initSearch = function(rawSearchIndex) {
query.literalSearch = true;
}
/**
* Increase the parser position as long as the character is a whitespace. This check is
* performed with the `isWhitespace` function.
*
* @param {ParserState} parserState
*/
function skipWhitespaces(parserState) {
while (parserState.pos < parserState.length) {
var c = parserState.userQuery[parserState.pos];
if (!isWhitespace(c)) {
break;
}
parserState.pos += 1;
}
}
/**
* Returns `true` if the current parser position is starting with "::".
*
@ -233,7 +213,6 @@ window.initSearch = function(rawSearchIndex) {
* @param {Array<QueryElement>} generics - List of generics of this query element.
*/
function createQueryElement(query, parserState, elems, name, generics) {
removeEmptyStringsFromArray(generics);
if (name === '*' || (name.length === 0 && generics.length === 0)) {
return;
}
@ -241,7 +220,20 @@ window.initSearch = function(rawSearchIndex) {
throw new Error("You cannot have more than one element if you use quotes");
}
var pathSegments = name.split("::");
removeEmptyStringsFromArray(pathSegments);
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 but it remains valid.
if (pathSegments.length === 0) {
pathSegments = [""];
@ -272,7 +264,6 @@ window.initSearch = function(rawSearchIndex) {
start += 1;
getStringElem(query, parserState, isInGenerics);
end = parserState.pos - 1;
skipWhitespaces(parserState);
} else {
while (parserState.pos < parserState.length) {
var c = parserState.userQuery[parserState.pos];
@ -289,7 +280,6 @@ window.initSearch = function(rawSearchIndex) {
}
parserState.pos += 1;
end = parserState.pos;
skipWhitespaces(parserState);
}
}
if (parserState.pos < parserState.length &&
@ -317,22 +307,36 @@ window.initSearch = function(rawSearchIndex) {
* character.
*/
function getItemsBefore(query, parserState, elems, limit) {
var turns = 0;
while (parserState.pos < parserState.length) {
var c = parserState.userQuery[parserState.pos];
if (c === limit) {
break;
} else if (c === '(' || c === ":") {
// Something weird is going on in here. Ignoring it!
} else if (c === "," && limit !== "" && turns > 0) {
parserState.pos += 1;
continue;
} else if (c === ":" && isPathStart(parserState)) {
throw new Error("Unexpected `::`: paths cannot start with `::`");
} else if (c === "(" || c === ":" || isEndCharacter(c)) {
var extra = "";
if (limit === ">") {
extra = "`<`";
} else if (limit === ")") {
extra = "`(`";
} else if (limit === "") {
extra = "`->`";
}
throw new Error("Unexpected `" + c + "` after " + extra);
}
var posBefore = parserState.pos;
getNextElem(query, parserState, elems, limit === ">");
turns += 1;
if (posBefore === parserState.pos) {
parserState.pos += 1;
}
}
// We skip the "limit".
// We are either at the end of the string or on the "limit" character, let's move forward
// in any case.
parserState.pos += 1;
}
@ -356,9 +360,13 @@ window.initSearch = function(rawSearchIndex) {
break;
} else if (c === ":" &&
parserState.typeFilter === null &&
!isPathStart(parserState) &&
query.elems.length === 1)
!isPathStart(parserState))
{
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 `:`");
}
if (query.literalSearch) {
throw new Error("You cannot use quotes on type filter");
}
@ -531,6 +539,10 @@ window.initSearch = function(rawSearchIndex) {
try {
parseInput(query, parserState);
if (parserState.typeFilter !== null) {
var typeFilter = parserState.typeFilter.replace(/^const$/, "constant");
query.typeFilter = itemTypeFromName(typeFilter);
}
} catch (err) {
query = newParsedQuery(userQuery);
query.error = err.message;
@ -548,10 +560,6 @@ window.initSearch = function(rawSearchIndex) {
createQueryElement(query, parserState, query.elems, userQuery, []);
query.foundElems += 1;
}
if (parserState.typeFilter !== null) {
var typeFilter = parserState.typeFilter.replace(/^const$/, "constant");
query.typeFilter = itemTypeFromName(typeFilter);
}
return query;
}
@ -582,9 +590,6 @@ window.initSearch = function(rawSearchIndex) {
* @return {ResultsTable}
*/
function execQuery(parsedQuery, searchWords, filterCrates) {
if (parsedQuery.error !== null) {
createQueryResults([], [], [], parsedQuery);
}
var results_others = {}, results_in_args = {}, results_returned = {};
function transformResults(results) {
@ -1267,7 +1272,10 @@ window.initSearch = function(rawSearchIndex) {
}
}
}
innerRunQuery();
if (parsedQuery.error === null) {
innerRunQuery();
}
var ret = createQueryResults(
sortResults(results_in_args, true),
@ -1275,6 +1283,10 @@ window.initSearch = function(rawSearchIndex) {
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;
}
@ -1413,7 +1425,7 @@ window.initSearch = function(rawSearchIndex) {
var output = document.createElement("div");
var length = 0;
if (array.length > 0 && query.error === null) {
if (array.length > 0) {
output.className = "search-results " + extraClass;
array.forEach(function(item) {
@ -1466,10 +1478,7 @@ window.initSearch = function(rawSearchIndex) {
link.appendChild(wrapper);
output.appendChild(link);
});
} else if (query.error !== null) {
output.className = "search-failed" + extraClass;
output.innerHTML = "Syntax error: " + query.error;
} else {
} else if (query.error === null) {
output.className = "search-failed" + extraClass;
output.innerHTML = "No results :(<br/>" +
"Try on <a href=\"https://duckduckgo.com/?q=" +
@ -1552,15 +1561,19 @@ window.initSearch = function(rawSearchIndex) {
}
crates += `</select>`;
}
var typeFilter = "";
if (results.query.typeFilter !== NO_TYPE_FILTER) {
typeFilter = " (type: " + escape(results.query.typeFilter) + ")";
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>` +
`<div id="titles">` +
`${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]) +

View File

@ -1,4 +1,21 @@
const QUERY = ['<"P">', '"P" "P"', 'P "P"', '"p" p', '"const": p'];
const QUERY = [
'<"P">',
'"P" "P"',
'P "P"',
'"p" p',
'"const": p',
"<:a>", "<::a>",
"((a))",
"->,a",
"(p -> p",
"::a::b",
"a::::b",
"a::b::",
":a",
"a b:",
"a (b:",
"{:",
];
const PARSED = [
{
@ -51,4 +68,124 @@ const PARSED = [
userQuery: "\"const\": p",
error: "You cannot use quotes on type filter",
},
{
args: [],
elems: [],
foundElems: 0,
original: "<:a>",
returned: [],
typeFilter: -1,
userQuery: "<:a>",
error: "Unexpected `:` after `<`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "<::a>",
returned: [],
typeFilter: -1,
userQuery: "<::a>",
error: "Unexpected `::`: paths cannot start with `::`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "((a))",
returned: [],
typeFilter: -1,
userQuery: "((a))",
error: "Unexpected `(` after `(`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "->,a",
returned: [],
typeFilter: -1,
userQuery: "->,a",
error: "Unexpected `,` after `->`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "(p -> p",
returned: [],
typeFilter: -1,
userQuery: "(p -> p",
error: "Unexpected `-` after `(`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "::a::b",
returned: [],
typeFilter: -1,
userQuery: "::a::b",
error: "Paths cannot start with `::`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "a::::b",
returned: [],
typeFilter: -1,
userQuery: "a::::b",
error: "Unexpected `::::`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "a::b::",
returned: [],
typeFilter: -1,
userQuery: "a::b::",
error: "Paths cannot end with `::`",
},
{
args: [],
elems: [],
foundElems: 0,
original: ":a",
returned: [],
typeFilter: -1,
userQuery: ":a",
error: "Expected type filter before `:`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "a b:",
returned: [],
typeFilter: -1,
userQuery: "a b:",
error: "Unexpected `:`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "a (b:",
returned: [],
typeFilter: -1,
userQuery: "a (b:",
error: "Unexpected `:` after `(`",
},
{
args: [],
elems: [],
foundElems: 0,
original: "{:",
returned: [],
typeFilter: -1,
userQuery: "{:",
error: "Unknown type filter `{`",
},
];

View File

@ -35,18 +35,12 @@ const PARSED = [
},
{
args: [],
elems: [{
name: "foo",
fullPath: ["foo"],
pathWithoutLast: [],
pathLast: "foo",
generics: [],
}],
foundElems: 1,
elems: [],
foundElems: 0,
original: "macro<f>:foo",
returned: [],
typeFilter: 14,
typeFilter: -1,
userQuery: "macro<f>:foo",
error: null,
error: "Unexpected `:`",
},
];

View File

@ -1,4 +1,4 @@
const QUERY = ['<P>', 'A<B<C<D>, E>', 'p<> u8'];
const QUERY = ['<P>', 'A<B<C<D>, E>', 'p<> u8'];
const PARSED = [
{
@ -66,10 +66,10 @@ const PARSED = [
],
}],
foundElems: 1,
original: 'A<B<C<D>, E>',
original: 'A<B<C<D>, E>',
returned: [],
typeFilter: -1,
userQuery: 'a<b<c<d>, e>',
userQuery: 'a<b<c<d>, e>',
error: null,
},
{

View File

@ -1,83 +1,57 @@
// 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 = ['-> <P> (p2)', '(p -> p2', 'a b', 'a,b(c)'];
const QUERY = ['a b', 'a b', 'a,b(c)'];
const PARSED = [
{
args: [],
elems: [],
foundElems: 2,
original: "-> <P> (p2)",
returned: [
elems: [
{
name: "",
fullPath: [""],
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "",
generics: [
{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
},
],
pathLast: "a",
generics: [],
},
{
name: "p2",
fullPath: ["p2"],
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "p2",
pathLast: "b",
generics: [],
},
],
typeFilter: -1,
userQuery: "-> <p> (p2)",
error: null,
},
{
args: [
{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
},
{
name: "p2",
fullPath: ["p2"],
pathWithoutLast: [],
pathLast: "p2",
generics: [],
},
],
elems: [],
foundElems: 2,
original: "(p -> p2",
original: "a b",
returned: [],
typeFilter: -1,
userQuery: "(p -> p2",
userQuery: "a b",
error: null,
},
{
args: [],
elems: [
{
name: "a b",
fullPath: ["a b"],
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "a b",
pathLast: "a",
generics: [],
},
{
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "b",
generics: [],
},
],
foundElems: 1,
original: "a b",
foundElems: 2,
original: "a b",
returned: [],
typeFilter: -1,
userQuery: "a b",
userQuery: "a b",
error: null,
},
{

View File

@ -1,4 +1,4 @@
const QUERY = ['A::B', '::A::B', 'A::B::,C', 'A::B::<f>,C'];
const QUERY = ['A::B', 'A::B,C', 'A::B<f>,C'];
const PARSED = [
{
@ -17,27 +17,11 @@ const PARSED = [
userQuery: "a::b",
error: null,
},
{
args: [],
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,
},
{
args: [],
elems: [
{
name: "a::b::",
name: "a::b",
fullPath: ["a", "b"],
pathWithoutLast: ["a"],
pathLast: "b",
@ -52,17 +36,17 @@ const PARSED = [
},
],
foundElems: 2,
original: 'A::B::,C',
original: 'A::B,C',
returned: [],
typeFilter: -1,
userQuery: 'a::b::,c',
userQuery: 'a::b,c',
error: null,
},
{
args: [],
elems: [
{
name: "a::b::",
name: "a::b",
fullPath: ["a", "b"],
pathWithoutLast: ["a"],
pathLast: "b",
@ -85,10 +69,10 @@ const PARSED = [
},
],
foundElems: 2,
original: 'A::B::<f>,C',
original: 'A::B<f>,C',
returned: [],
typeFilter: -1,
userQuery: 'a::b::<f>,c',
userQuery: 'a::b<f>,c',
error: null,
},
];

View File

@ -272,10 +272,9 @@ function loadSearchJsAndIndex(searchJs, searchIndex, storageJs, crate) {
var functionsToLoad = ["buildHrefAndPath", "pathSplitter", "levenshtein", "validateResult",
"buildIndex", "execQuery", "parseQuery", "createQueryResults",
"isWhitespace", "isSpecialStartCharacter", "isStopCharacter",
"removeEmptyStringsFromArray", "parseInput", "getItemsBefore",
"getNextElem", "createQueryElement", "isReturnArrow", "isPathStart",
"skipWhitespaces", "getStringElem", "itemTypeFromName",
"newParsedQuery"];
"parseInput", "getItemsBefore", "getNextElem", "createQueryElement",
"isReturnArrow", "isPathStart", "getStringElem", "newParsedQuery",
"itemTypeFromName", "isEndCharacter"];
const functions = ["hasOwnPropertyRustdoc", "onEach"];
ALIASES = {};