"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _lodash = _interopRequireDefault(require("lodash"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const schema = [{
  enum: ['always', 'never'],
  type: 'string'
}, {
  additionalProperties: false,
  properties: {
    annotateUndefined: {
      enum: ['always', 'never', 'ignore', 'always-enforce'],
      type: 'string'
    },
    excludeArrowFunctions: {
      enum: [false, true, 'expressionsOnly']
    },
    excludeMatching: {
      items: {
        type: 'string'
      },
      type: 'array'
    },
    includeOnlyMatching: {
      items: {
        type: 'string'
      },
      type: 'array'
    }
  },
  type: 'object'
}];

const makeRegExp = str => {
  return new RegExp(str, 'u');
};

const isUndefinedReturnType = returnNode => {
  return returnNode.argument === null || returnNode.argument.name === 'undefined' || returnNode.argument.operator === 'void';
};

const create = context => {
  const annotateReturn = (_lodash.default.get(context, 'options[0]') || 'always') === 'always';
  const annotateUndefined = _lodash.default.get(context, 'options[1].annotateUndefined') || 'never';
  const skipArrows = _lodash.default.get(context, 'options[1].excludeArrowFunctions') || false; // eslint-disable-next-line unicorn/no-array-callback-reference

  const excludeMatching = _lodash.default.get(context, 'options[1].excludeMatching', []).map(makeRegExp); // eslint-disable-next-line unicorn/no-array-callback-reference


  const includeOnlyMatching = _lodash.default.get(context, 'options[1].includeOnlyMatching', []).map(makeRegExp);

  const targetNodes = [];

  const registerFunction = functionNode => {
    targetNodes.push({
      functionNode
    });
  };

  const getIsReturnTypeAnnotationUndefined = targetNode => {
    const isReturnTypeAnnotationLiteralUndefined = _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.id.name') === 'undefined' && _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.type') === 'GenericTypeAnnotation';
    const isReturnTypeAnnotationVoid = _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.type') === 'VoidTypeAnnotation';
    const isAsyncReturnTypeAnnotationVoid = _lodash.default.get(targetNode, 'functionNode.async') && _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.id.name') === 'Promise' && (_lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.typeParameters.params[0].type') === 'VoidTypeAnnotation' || _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.typeParameters.params[0].id.name') === 'undefined' && _lodash.default.get(targetNode, 'functionNode.returnType.typeAnnotation.typeParameters.params[0].type') === 'GenericTypeAnnotation');
    return isReturnTypeAnnotationLiteralUndefined || isReturnTypeAnnotationVoid || isAsyncReturnTypeAnnotationVoid;
  };

  const shouldFilterNode = functionNode => {
    const isArrow = functionNode.type === 'ArrowFunctionExpression';
    const isMethod = functionNode.parent && functionNode.parent.type === 'MethodDefinition';
    const propertyNodes = ['Property', 'ClassProperty'];
    const isProperty = functionNode.parent && propertyNodes.includes(functionNode.parent.type);
    let selector;

    if (isMethod || isProperty) {
      selector = 'parent.key.name';
    } else if (isArrow) {
      selector = 'parent.id.name';
    } else {
      selector = 'id.name';
    }

    const identifierName = _lodash.default.get(functionNode, selector);

    const checkRegExp = regex => {
      return regex.test(identifierName);
    };

    if (excludeMatching.length && _lodash.default.some(excludeMatching, checkRegExp)) {
      return true;
    }

    if (includeOnlyMatching.length && !_lodash.default.some(includeOnlyMatching, checkRegExp)) {
      return true;
    }

    return false;
  }; // eslint-disable-next-line complexity


  const evaluateFunction = functionNode => {
    const targetNode = targetNodes.pop();

    if (functionNode !== targetNode.functionNode) {
      throw new Error('Mismatch.');
    }

    const isArrow = functionNode.type === 'ArrowFunctionExpression';
    const isArrowFunctionExpression = functionNode.expression;
    const isFunctionReturnUndefined = !isArrowFunctionExpression && !functionNode.generator && (!targetNode.returnStatementNode || isUndefinedReturnType(targetNode.returnStatementNode));
    const isReturnTypeAnnotationUndefined = getIsReturnTypeAnnotationUndefined(targetNode);

    if (skipArrows === 'expressionsOnly' && isArrowFunctionExpression || skipArrows === true && isArrow || shouldFilterNode(functionNode)) {
      return;
    }

    const returnType = functionNode.returnType || isArrow && _lodash.default.get(functionNode, 'parent.id.typeAnnotation');

    if (isFunctionReturnUndefined && isReturnTypeAnnotationUndefined && annotateUndefined === 'never') {
      context.report({
        message: 'Must not annotate undefined return type.',
        node: functionNode
      });
    } else if (isFunctionReturnUndefined && !isReturnTypeAnnotationUndefined && annotateUndefined === 'always') {
      context.report({
        message: 'Must annotate undefined return type.',
        node: functionNode
      });
    } else if ((annotateUndefined === 'always-enforce' || !isFunctionReturnUndefined && !isReturnTypeAnnotationUndefined) && annotateReturn && !returnType && !shouldFilterNode(functionNode)) {
      context.report({
        message: 'Missing return type annotation.',
        node: functionNode
      });
    }
  };

  const evaluateNoise = () => {
    targetNodes.pop();
  };

  return {
    ArrowFunctionExpression: registerFunction,
    'ArrowFunctionExpression:exit': evaluateFunction,
    ClassDeclaration: registerFunction,
    'ClassDeclaration:exit': evaluateNoise,
    ClassExpression: registerFunction,
    'ClassExpression:exit': evaluateNoise,
    FunctionDeclaration: registerFunction,
    'FunctionDeclaration:exit': evaluateFunction,
    FunctionExpression: registerFunction,
    'FunctionExpression:exit': evaluateFunction,
    ReturnStatement: node => {
      if (targetNodes.length) {
        targetNodes[targetNodes.length - 1].returnStatementNode = node;
      }
    }
  };
};

var _default = {
  create,
  schema
};
exports.default = _default;
module.exports = exports.default;