export function validateFunction()

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}`);
}