mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ES|QL] Stop autocomplete on non-existent function (#175675)
## Summary
This PR fixes a scenario where the user types a non-existent function
into the editor:
* before a JS error was logged into the console due to a forced case
into `isFunctionArgComplete` function
* before eval functions were suggested by default in this case, now the
service just stops with an early exit.

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
parent
63c3ef4808
commit
3f92578e61
2 changed files with 122 additions and 103 deletions
|
@ -625,6 +625,19 @@ describe('autocomplete', () => {
|
|||
],
|
||||
'('
|
||||
);
|
||||
testSuggestions(
|
||||
'from a | eval a=raund()', // note the typo in round
|
||||
[],
|
||||
'('
|
||||
);
|
||||
testSuggestions(
|
||||
'from a | eval a=raund(', // note the typo in round
|
||||
[]
|
||||
);
|
||||
testSuggestions(
|
||||
'from a | eval raund(', // note the typo in round
|
||||
[]
|
||||
);
|
||||
testSuggestions('from a | eval a=round(numberField) ', [
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['number']),
|
||||
'|',
|
||||
|
|
|
@ -48,7 +48,6 @@ import {
|
|||
commandAutocompleteDefinitions,
|
||||
getAssignmentDefinitionCompletitionItem,
|
||||
getBuiltinCompatibleFunctionDefinition,
|
||||
mathCommandDefinition,
|
||||
pipeCompleteItem,
|
||||
} from './complete_items';
|
||||
import {
|
||||
|
@ -402,7 +401,10 @@ function isFunctionArgComplete(
|
|||
arg: ESQLFunction,
|
||||
references: Pick<ReferenceMaps, 'fields' | 'variables'>
|
||||
) {
|
||||
const fnDefinition = getFunctionDefinition(arg.name)!;
|
||||
const fnDefinition = getFunctionDefinition(arg.name);
|
||||
if (!fnDefinition) {
|
||||
return { complete: false };
|
||||
}
|
||||
const cleanedArgs = removeMarkerArgFromArgsList(arg)!.args;
|
||||
const argLengthCheck = fnDefinition.signatures.some((def) => {
|
||||
if (def.infiniteParams && cleanedArgs.length > 0) {
|
||||
|
@ -478,6 +480,11 @@ async function getExpressionSuggestionsByType(
|
|||
// * or after a comma => i.e. STATS fieldA, <here>
|
||||
const isNewExpression = isRestartingExpression(innerText) || argIndex === 0;
|
||||
|
||||
// early exit in case of a missing function
|
||||
if (isFunctionItem(lastArg) && !getFunctionDefinition(lastArg.name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Are options already declared? This is useful to suggest only new ones
|
||||
const optionsAlreadyDeclared = (
|
||||
command.args.filter((arg) => isOptionItem(arg)) as ESQLCommandOption[]
|
||||
|
@ -925,108 +932,107 @@ async function getFunctionArgsSuggestions(
|
|||
getPolicyMetadata: GetPolicyMetadataFn
|
||||
): Promise<AutocompleteCommandDefinition[]> {
|
||||
const fnDefinition = getFunctionDefinition(node.name);
|
||||
if (fnDefinition) {
|
||||
const fieldsMap: Map<string, ESQLRealField> = await getFieldsMap();
|
||||
const variablesExcludingCurrentCommandOnes = excludeVariablesFromCurrentCommand(
|
||||
commands,
|
||||
command,
|
||||
fieldsMap
|
||||
);
|
||||
// pick the type of the next arg
|
||||
const shouldGetNextArgument = node.text.includes(EDITOR_MARKER);
|
||||
let argIndex = Math.max(node.args.length, 0);
|
||||
if (!shouldGetNextArgument && argIndex) {
|
||||
argIndex -= 1;
|
||||
}
|
||||
const types = fnDefinition.signatures.flatMap((signature) => {
|
||||
if (signature.params.length > argIndex) {
|
||||
return signature.params[argIndex].type;
|
||||
}
|
||||
if (signature.infiniteParams) {
|
||||
return signature.params[0].type;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const arg = node.args[argIndex];
|
||||
|
||||
const hasMoreMandatoryArgs =
|
||||
fnDefinition.signatures[0].params.filter(
|
||||
({ optional }, index) => !optional && index > argIndex
|
||||
).length > argIndex;
|
||||
|
||||
const suggestions = [];
|
||||
const noArgDefined = !arg;
|
||||
const isUnknownColumn =
|
||||
arg &&
|
||||
isColumnItem(arg) &&
|
||||
!columnExists(arg, { fields: fieldsMap, variables: variablesExcludingCurrentCommandOnes })
|
||||
.hit;
|
||||
if (noArgDefined || isUnknownColumn) {
|
||||
// ... | EVAL fn( <suggest>)
|
||||
// ... | EVAL fn( field, <suggest>)
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
types,
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: command.name !== 'stats',
|
||||
fields: true,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
},
|
||||
// do not repropose the same function as arg
|
||||
// i.e. avoid cases like abs(abs(abs(...))) with suggestions
|
||||
{ ignoreFn: [node.name] }
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// for eval and row commands try also to complete numeric literals with time intervals where possible
|
||||
if (arg) {
|
||||
if (command.name !== 'stats') {
|
||||
if (isLiteralItem(arg) && arg.literalType === 'number') {
|
||||
// ... | EVAL fn(2 <suggest>)
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
['time_literal_unit'],
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: false,
|
||||
fields: false,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
if (hasMoreMandatoryArgs) {
|
||||
// suggest a comma if there's another argument for the function
|
||||
suggestions.push(commaCompleteItem);
|
||||
}
|
||||
// if there are other arguments in the function, inject automatically a comma after each suggestion
|
||||
return suggestions.map((suggestion) =>
|
||||
suggestion !== commaCompleteItem
|
||||
? {
|
||||
...suggestion,
|
||||
insertText:
|
||||
hasMoreMandatoryArgs && !fnDefinition.builtin
|
||||
? `${suggestion.insertText},`
|
||||
: suggestion.insertText,
|
||||
}
|
||||
: suggestion
|
||||
);
|
||||
}
|
||||
|
||||
return suggestions.map(({ insertText, ...rest }) => ({
|
||||
...rest,
|
||||
insertText: hasMoreMandatoryArgs && !fnDefinition.builtin ? `${insertText},` : insertText,
|
||||
}));
|
||||
// early exit on no hit
|
||||
if (!fnDefinition) {
|
||||
return [];
|
||||
}
|
||||
return mathCommandDefinition;
|
||||
const fieldsMap: Map<string, ESQLRealField> = await getFieldsMap();
|
||||
const variablesExcludingCurrentCommandOnes = excludeVariablesFromCurrentCommand(
|
||||
commands,
|
||||
command,
|
||||
fieldsMap
|
||||
);
|
||||
// pick the type of the next arg
|
||||
const shouldGetNextArgument = node.text.includes(EDITOR_MARKER);
|
||||
let argIndex = Math.max(node.args.length, 0);
|
||||
if (!shouldGetNextArgument && argIndex) {
|
||||
argIndex -= 1;
|
||||
}
|
||||
const types = fnDefinition.signatures.flatMap((signature) => {
|
||||
if (signature.params.length > argIndex) {
|
||||
return signature.params[argIndex].type;
|
||||
}
|
||||
if (signature.infiniteParams) {
|
||||
return signature.params[0].type;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const arg = node.args[argIndex];
|
||||
|
||||
const hasMoreMandatoryArgs =
|
||||
fnDefinition.signatures[0].params.filter(({ optional }, index) => !optional && index > argIndex)
|
||||
.length > argIndex;
|
||||
|
||||
const suggestions = [];
|
||||
const noArgDefined = !arg;
|
||||
const isUnknownColumn =
|
||||
arg &&
|
||||
isColumnItem(arg) &&
|
||||
!columnExists(arg, { fields: fieldsMap, variables: variablesExcludingCurrentCommandOnes }).hit;
|
||||
if (noArgDefined || isUnknownColumn) {
|
||||
// ... | EVAL fn( <suggest>)
|
||||
// ... | EVAL fn( field, <suggest>)
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
types,
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: command.name !== 'stats',
|
||||
fields: true,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
},
|
||||
// do not repropose the same function as arg
|
||||
// i.e. avoid cases like abs(abs(abs(...))) with suggestions
|
||||
{ ignoreFn: [node.name] }
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// for eval and row commands try also to complete numeric literals with time intervals where possible
|
||||
if (arg) {
|
||||
if (command.name !== 'stats') {
|
||||
if (isLiteralItem(arg) && arg.literalType === 'number') {
|
||||
// ... | EVAL fn(2 <suggest>)
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
['time_literal_unit'],
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: false,
|
||||
fields: false,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
if (hasMoreMandatoryArgs) {
|
||||
// suggest a comma if there's another argument for the function
|
||||
suggestions.push(commaCompleteItem);
|
||||
}
|
||||
// if there are other arguments in the function, inject automatically a comma after each suggestion
|
||||
return suggestions.map((suggestion) =>
|
||||
suggestion !== commaCompleteItem
|
||||
? {
|
||||
...suggestion,
|
||||
insertText:
|
||||
hasMoreMandatoryArgs && !fnDefinition.builtin
|
||||
? `${suggestion.insertText},`
|
||||
: suggestion.insertText,
|
||||
}
|
||||
: suggestion
|
||||
);
|
||||
}
|
||||
|
||||
return suggestions.map(({ insertText, ...rest }) => ({
|
||||
...rest,
|
||||
insertText: hasMoreMandatoryArgs && !fnDefinition.builtin ? `${insertText},` : insertText,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getSettingArgsSuggestions(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue