in src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts [51:330]
export function validateFunction({
fn,
parentCommand,
parentOption,
references,
forceConstantOnly = false,
isNested,
parentAst,
currentCommandIndex,
}: {
fn: ESQLFunction;
parentCommand: string;
parentOption?: string;
references: ReferenceMaps;
forceConstantOnly?: boolean;
isNested?: boolean;
parentAst?: ESQLCommand[];
currentCommandIndex?: number;
}): ESQLMessage[] {
const messages: ESQLMessage[] = [];
if (fn.incomplete) {
return messages;
}
if (isFunctionOperatorParam(fn)) {
return messages;
}
const fnDefinition = getFunctionDefinition(fn.name)!;
const isFnSupported = isSupportedFunction(fn.name, parentCommand, parentOption);
if (typeof textSearchFunctionsValidators[fn.name] === 'function') {
const validator = textSearchFunctionsValidators[fn.name];
messages.push(
...validator({
fn,
parentCommand,
parentOption,
references,
isNested,
parentAst,
currentCommandIndex,
})
);
}
if (!isFnSupported.supported) {
if (isFnSupported.reason === 'unknownFunction') {
messages.push(errors.unknownFunction(fn));
}
// for nested functions skip this check and make the nested check fail later on
if (isFnSupported.reason === 'unsupportedFunction' && !isNested) {
messages.push(
parentOption
? getMessageFromId({
messageId: 'unsupportedFunctionForCommandOption',
values: {
name: fn.name,
command: parentCommand.toUpperCase(),
option: parentOption.toUpperCase(),
},
locations: fn.location,
})
: getMessageFromId({
messageId: 'unsupportedFunctionForCommand',
values: { name: fn.name, command: parentCommand.toUpperCase() },
locations: fn.location,
})
);
}
if (messages.length) {
return messages;
}
}
const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, fn);
if (!matchingSignatures.length) {
const { max, min } = getMaxMinNumberOfParams(fnDefinition);
if (max === min) {
messages.push(
getMessageFromId({
messageId: 'wrongArgumentNumber',
values: {
fn: fn.name,
numArgs: max,
passedArgs: fn.args.length,
},
locations: fn.location,
})
);
} else if (fn.args.length > max) {
messages.push(
getMessageFromId({
messageId: 'wrongArgumentNumberTooMany',
values: {
fn: fn.name,
numArgs: max,
passedArgs: fn.args.length,
extraArgs: fn.args.length - max,
},
locations: fn.location,
})
);
} else {
messages.push(
getMessageFromId({
messageId: 'wrongArgumentNumberTooFew',
values: {
fn: fn.name,
numArgs: min,
passedArgs: fn.args.length,
missingArgs: min - fn.args.length,
},
locations: fn.location,
})
);
}
}
// now perform the same check on all functions args
for (let i = 0; i < fn.args.length; i++) {
const arg = fn.args[i];
const allMatchingArgDefinitionsAreConstantOnly = matchingSignatures.every((signature) => {
return signature.params[i]?.constantOnly;
});
const wrappedArray = Array.isArray(arg) ? arg : [arg];
for (const _subArg of wrappedArray) {
/**
* we need to remove the inline casts
* to see if there's a function under there
*
* e.g. for ABS(CEIL(numberField)::int), we need to validate CEIL(numberField)
*/
const subArg = removeInlineCasts(_subArg);
if (isFunctionItem(subArg)) {
const messagesFromArg = validateFunction({
fn: subArg,
parentCommand,
parentOption,
references,
/**
* The constantOnly constraint needs to be enforced for arguments that
* are functions as well, regardless of whether the definition for the
* sub function's arguments includes the constantOnly flag.
*
* Example:
* bucket(@timestamp, abs(bytes), "", "")
*
* In the above example, the abs function is not defined with the
* constantOnly flag, but the second parameter in bucket _is_ defined
* with the constantOnly flag.
*
* Because of this, the abs function's arguments inherit the constraint
* and each should be validated as if each were constantOnly.
*/
forceConstantOnly: allMatchingArgDefinitionsAreConstantOnly || forceConstantOnly,
// use the nesting flag for now just for stats and metrics
// TODO: revisit this part later on to make it more generic
isNested: ['stats', 'inlinestats', 'ts'].includes(parentCommand)
? isNested || !isAssignment(fn)
: false,
parentAst,
});
if (messagesFromArg.some(({ code }) => code === 'expectedConstant')) {
const consolidatedMessage = getMessageFromId({
messageId: 'expectedConstant',
values: {
fn: fn.name,
given: subArg.text,
},
locations: subArg.location,
});
messages.push(
consolidatedMessage,
...messagesFromArg.filter(({ code }) => code !== 'expectedConstant')
);
} else {
messages.push(...messagesFromArg);
}
}
}
}
// check if the definition has some specific validation to apply:
if (fnDefinition.validate) {
const payloads = fnDefinition.validate(fn);
if (payloads.length) {
messages.push(...payloads);
}
}
// at this point we're sure that at least one signature is matching
const failingSignatures: ESQLMessage[][] = [];
let relevantFuncSignatures = matchingSignatures;
const enrichedArgs = fn.args;
if (fn.name === 'in' || fn.name === 'not_in') {
for (let argIndex = 1; argIndex < fn.args.length; argIndex++) {
relevantFuncSignatures = fnDefinition.signatures.filter(
(s) =>
s.params?.length >= argIndex &&
s.params.slice(0, argIndex).every(({ type: dataType }, idx) => {
const arg = enrichedArgs[idx];
if (isLiteralItem(arg)) {
return (
dataType === arg.literalType || compareTypesWithLiterals(dataType, arg.literalType)
);
}
return false; // Non-literal arguments don't match
})
);
}
}
for (const signature of relevantFuncSignatures) {
const failingSignature: ESQLMessage[] = [];
fn.args.forEach((outerArg, index) => {
const argDef = getParamAtPosition(signature, index);
if ((!outerArg && argDef?.optional) || !argDef) {
// that's ok, just skip it
// the else case is already catched with the argument counts check
// few lines above
return;
}
// check every element of the argument (may be an array of elements, or may be a single element)
const hasMultipleElements = Array.isArray(outerArg);
const argElements = hasMultipleElements ? outerArg : [outerArg];
const singularType = extractSingularType(argDef.type);
const messagesFromAllArgElements = argElements.flatMap((arg) => {
return [
validateFunctionLiteralArg,
validateNestedFunctionArg,
validateFunctionColumnArg,
validateInlineCastArg,
].flatMap((validateFn) => {
return validateFn(
fn,
arg,
{
...argDef,
type: singularType,
constantOnly: forceConstantOnly || argDef.constantOnly,
},
references,
parentCommand
);
});
});
const shouldCollapseMessages = isArrayType(argDef.type as string) && hasMultipleElements;
failingSignature.push(
...(shouldCollapseMessages
? collapseWrongArgumentTypeMessages(
messagesFromAllArgElements,
outerArg,
fn.name,
argDef.type as string,
parentCommand,
references
)
: messagesFromAllArgElements)
);
});
if (failingSignature.length) {
failingSignatures.push(failingSignature);
}
}
if (failingSignatures.length && failingSignatures.length === relevantFuncSignatures.length) {
const failingSignatureOrderedByErrorCount = failingSignatures
.map((arr, index) => ({ index, count: arr.length }))
.sort((a, b) => a.count - b.count);
const indexForShortestFailingsignature = failingSignatureOrderedByErrorCount[0].index;
messages.push(...failingSignatures[indexForShortestFailingsignature]);
}
// This is due to a special case in enrich where an implicit assignment is possible
// so the AST needs to store an explicit "columnX = columnX" which duplicates the message
return uniqBy(messages, ({ location }) => `${location.min}-${location.max}`);
}