/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * 
 * @format
 */
'use strict';
/**
 * Transform match expressions and statements.
 */

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

var _SimpleTransform = require("../transform/SimpleTransform");

var _astNodeMutationHelpers = require("../transform/astNodeMutationHelpers");

var _createSyntaxError = require("../utils/createSyntaxError");

var _Builders = require("../utils/Builders");

var _GenID = require("../utils/GenID");

/**
 * Generated identifiers.
 * `GenID` is initialized in the transform.
 */
let GenID = null;

function genIdent() {
  if (GenID == null) {
    throw Error('GenID must be initialized at the start of the transform.');
  }

  return (0, _Builders.ident)(GenID.genID());
}
/**
 * A series of properties.
 * When combined with the match argument (the root expression), provides the
 * location of to be tested against, or location to be extracted to a binding.
 */


function objKeyToString(node) {
  switch (node.type) {
    case 'Identifier':
      return node.name;

    case 'Literal':
      {
        const {
          value
        } = node;

        if (typeof value === 'number') {
          return String(value);
        } else if (typeof value === 'string') {
          return value;
        } else {
          return node.raw;
        }
      }
  }
}

function convertMemberPattern(pattern) {
  const {
    base,
    property,
    loc,
    range
  } = pattern;
  const object = base.type === 'MatchIdentifierPattern' ? base.id : convertMemberPattern(base);

  if (property.type === 'Identifier') {
    return {
      type: 'MemberExpression',
      object,
      property,
      computed: false,
      optional: false,
      ...(0, _Builders.etc)({
        loc,
        range
      })
    };
  } else {
    return {
      type: 'MemberExpression',
      object,
      property,
      computed: true,
      optional: false,
      ...(0, _Builders.etc)({
        loc,
        range
      })
    };
  }
}

function checkDuplicateBindingName(seenBindingNames, node, name) {
  if (seenBindingNames.has(name)) {
    throw (0, _createSyntaxError.createSyntaxError)(node, `Duplicate variable name '${name}' in match case pattern.`);
  }

  seenBindingNames.add(name);
}

function checkBindingKind(node, kind) {
  if (kind === 'var') {
    throw (0, _createSyntaxError.createSyntaxError)(node, `'var' bindings are not allowed. Use 'const' or 'let'.`);
  }
}
/**
 * Does an object property's pattern require a `prop-exists` condition added?
 * If the pattern is a literal like `0`, then it's not required, since the `eq`
 * condition implies the prop exists. However, if we could be doing an equality
 * check against `undefined`, then it is required, since that will be true even
 * if the property doesn't exist.
 */


function needsPropExistsCond(pattern) {
  switch (pattern.type) {
    case 'MatchWildcardPattern':
    case 'MatchBindingPattern':
    case 'MatchIdentifierPattern':
    case 'MatchMemberPattern':
      return true;

    case 'MatchLiteralPattern':
    case 'MatchUnaryPattern':
    case 'MatchObjectPattern':
    case 'MatchArrayPattern':
      return false;

    case 'MatchAsPattern':
      {
        const {
          pattern: asPattern
        } = pattern;
        return needsPropExistsCond(asPattern);
      }

    case 'MatchOrPattern':
      {
        const {
          patterns
        } = pattern;
        return patterns.some(needsPropExistsCond);
      }
  }
}
/**
 * Analyzes a match pattern, and produced both the conditions and bindings
 * produced by that pattern.
 */


function analyzePattern(pattern, key, seenBindingNames) {
  switch (pattern.type) {
    case 'MatchWildcardPattern':
      {
        return {
          conditions: [],
          bindings: []
        };
      }

    case 'MatchLiteralPattern':
      {
        const {
          literal
        } = pattern;
        const condition = {
          type: 'eq',
          key,
          arg: literal
        };
        return {
          conditions: [condition],
          bindings: []
        };
      }

    case 'MatchUnaryPattern':
      {
        const {
          operator,
          argument,
          loc,
          range
        } = pattern;

        if (argument.value === 0) {
          // We haven't decided whether we will compare these using `===` or `Object.is`
          throw (0, _createSyntaxError.createSyntaxError)(pattern, `'+0' and '-0' are not yet supported in match unary patterns.`);
        }

        const arg = {
          type: 'UnaryExpression',
          operator,
          argument,
          prefix: true,
          ...(0, _Builders.etc)({
            loc,
            range
          })
        };
        const condition = {
          type: 'eq',
          key,
          arg
        };
        return {
          conditions: [condition],
          bindings: []
        };
      }

    case 'MatchIdentifierPattern':
      {
        const {
          id
        } = pattern;
        const condition = id.name === 'NaN' ? {
          type: 'is-nan',
          key
        } : {
          type: 'eq',
          key,
          arg: id
        };
        return {
          conditions: [condition],
          bindings: []
        };
      }

    case 'MatchMemberPattern':
      {
        const arg = convertMemberPattern(pattern);
        const condition = {
          type: 'eq',
          key,
          arg
        };
        return {
          conditions: [condition],
          bindings: []
        };
      }

    case 'MatchBindingPattern':
      {
        const {
          id,
          kind
        } = pattern;
        checkDuplicateBindingName(seenBindingNames, pattern, id.name);
        checkBindingKind(pattern, kind);
        const binding = {
          type: 'id',
          key,
          kind,
          id
        };
        return {
          conditions: [],
          bindings: [binding]
        };
      }

    case 'MatchAsPattern':
      {
        const {
          pattern: asPattern,
          target
        } = pattern;

        if (asPattern.type === 'MatchBindingPattern') {
          throw (0, _createSyntaxError.createSyntaxError)(pattern, `Match 'as' patterns are not allowed directly on binding patterns.`);
        }

        const {
          conditions,
          bindings
        } = analyzePattern(asPattern, key, seenBindingNames);
        const [id, kind] = target.type === 'MatchBindingPattern' ? [target.id, target.kind] : [target, 'const'];
        checkDuplicateBindingName(seenBindingNames, pattern, id.name);
        checkBindingKind(pattern, kind);
        const binding = {
          type: 'id',
          key,
          kind,
          id
        };
        return {
          conditions,
          bindings: bindings.concat(binding)
        };
      }

    case 'MatchArrayPattern':
      {
        const {
          elements,
          rest
        } = pattern;
        const lengthOp = rest == null ? 'eq' : 'gte';
        const conditions = [{
          type: 'array',
          key,
          length: elements.length,
          lengthOp
        }];
        const bindings = [];
        elements.forEach((element, i) => {
          const elementKey = key.concat((0, _Builders.numberLiteral)(i));
          const {
            conditions: childConditions,
            bindings: childBindings
          } = analyzePattern(element, elementKey, seenBindingNames);
          conditions.push(...childConditions);
          bindings.push(...childBindings);
        });

        if (rest != null && rest.argument != null) {
          const {
            id,
            kind
          } = rest.argument;
          checkDuplicateBindingName(seenBindingNames, rest.argument, id.name);
          checkBindingKind(pattern, kind);
          bindings.push({
            type: 'array-rest',
            key,
            exclude: elements.length,
            kind,
            id
          });
        }

        return {
          conditions,
          bindings
        };
      }

    case 'MatchObjectPattern':
      {
        const {
          properties,
          rest
        } = pattern;
        const conditions = [{
          type: 'object',
          key
        }];
        const bindings = [];
        const objKeys = [];
        const seenNames = new Set();
        properties.forEach(prop => {
          const {
            key: objKey,
            pattern: propPattern
          } = prop;
          objKeys.push(objKey);
          const name = objKeyToString(objKey);

          if (seenNames.has(name)) {
            throw (0, _createSyntaxError.createSyntaxError)(propPattern, `Duplicate property name '${name}' in match object pattern.`);
          }

          seenNames.add(name);
          const propKey = key.concat(objKey);

          if (needsPropExistsCond(propPattern)) {
            conditions.push({
              type: 'prop-exists',
              key,
              propName: name
            });
          }

          const {
            conditions: childConditions,
            bindings: childBindings
          } = analyzePattern(propPattern, propKey, seenBindingNames);
          conditions.push(...childConditions);
          bindings.push(...childBindings);
        });

        if (rest != null && rest.argument != null) {
          const {
            id,
            kind
          } = rest.argument;
          checkDuplicateBindingName(seenBindingNames, rest.argument, id.name);
          checkBindingKind(pattern, kind);
          bindings.push({
            type: 'object-rest',
            key,
            exclude: objKeys,
            kind,
            id
          });
        }

        return {
          conditions,
          bindings
        };
      }

    case 'MatchOrPattern':
      {
        const {
          patterns
        } = pattern;
        let hasWildcard = false;
        const orConditions = patterns.map(subpattern => {
          const {
            conditions,
            bindings
          } = analyzePattern(subpattern, key, seenBindingNames);

          if (bindings.length > 0) {
            // We will implement this in the future.
            throw (0, _createSyntaxError.createSyntaxError)(pattern, `Bindings in match 'or' patterns are not yet supported.`);
          }

          if (conditions.length === 0) {
            hasWildcard = true;
          }

          return conditions;
        });

        if (hasWildcard) {
          return {
            conditions: [],
            bindings: []
          };
        }

        return {
          conditions: [{
            type: 'or',
            orConditions
          }],
          bindings: []
        };
      }
  }
}

function expressionOfKey(root, key) {
  return key.reduce((acc, prop) => prop.type === 'Identifier' ? {
    type: 'MemberExpression',
    object: acc,
    property: (0, _astNodeMutationHelpers.shallowCloneNode)(prop),
    computed: false,
    optional: false,
    ...(0, _Builders.etc)()
  } : {
    type: 'MemberExpression',
    object: acc,
    property: (0, _astNodeMutationHelpers.shallowCloneNode)(prop),
    computed: true,
    optional: false,
    ...(0, _Builders.etc)()
  }, (0, _astNodeMutationHelpers.deepCloneNode)(root));
}

function testsOfCondition(root, condition) {
  switch (condition.type) {
    case 'eq':
      {
        // <x> === <arg>
        const {
          key,
          arg
        } = condition;
        return [{
          type: 'BinaryExpression',
          left: expressionOfKey(root, key),
          right: arg,
          operator: '===',
          ...(0, _Builders.etc)()
        }];
      }

    case 'is-nan':
      {
        // Number.isNaN(<x>)
        const {
          key
        } = condition;
        const callee = {
          type: 'MemberExpression',
          object: (0, _Builders.ident)('Number'),
          property: (0, _Builders.ident)('isNaN'),
          computed: false,
          optional: false,
          ...(0, _Builders.etc)()
        };
        return [(0, _Builders.callExpression)(callee, [expressionOfKey(root, key)])];
      }

    case 'array':
      {
        // Array.isArray(<x>) && <x>.length === <length>
        const {
          key,
          length,
          lengthOp
        } = condition;
        const operator = lengthOp === 'eq' ? '===' : '>=';
        const isArray = (0, _Builders.callExpression)({
          type: 'MemberExpression',
          object: (0, _Builders.ident)('Array'),
          property: (0, _Builders.ident)('isArray'),
          computed: false,
          optional: false,
          ...(0, _Builders.etc)()
        }, [expressionOfKey(root, key)]);
        const lengthCheck = {
          type: 'BinaryExpression',
          left: {
            type: 'MemberExpression',
            object: expressionOfKey(root, key),
            property: (0, _Builders.ident)('length'),
            computed: false,
            optional: false,
            ...(0, _Builders.etc)()
          },
          right: (0, _Builders.numberLiteral)(length),
          operator,
          ...(0, _Builders.etc)()
        };
        return [isArray, lengthCheck];
      }

    case 'object':
      {
        // (typeof <x> === 'object' && <x> !== null) || typeof <x> === 'function'
        const {
          key
        } = condition;
        const typeofObject = (0, _Builders.typeofExpression)(expressionOfKey(root, key), 'object');
        const typeofFunction = (0, _Builders.typeofExpression)(expressionOfKey(root, key), 'function');
        const notNull = {
          type: 'BinaryExpression',
          left: expressionOfKey(root, key),
          right: (0, _Builders.nullLiteral)(),
          operator: '!==',
          ...(0, _Builders.etc)()
        };
        return [(0, _Builders.disjunction)([(0, _Builders.conjunction)([typeofObject, notNull]), typeofFunction])];
      }

    case 'prop-exists':
      {
        // <propName> in <x>
        const {
          key,
          propName
        } = condition;
        const inObject = {
          type: 'BinaryExpression',
          left: (0, _Builders.stringLiteral)(propName),
          right: expressionOfKey(root, key),
          operator: 'in',
          ...(0, _Builders.etc)()
        };
        return [inObject];
      }

    case 'or':
      {
        // <a> || <b> || ...
        const {
          orConditions
        } = condition;
        const tests = orConditions.map(conditions => (0, _Builders.conjunction)(testsOfConditions(root, conditions)));
        return [(0, _Builders.disjunction)(tests)];
      }
  }
}

function testsOfConditions(root, conditions) {
  return conditions.flatMap(condition => testsOfCondition(root, condition));
}

function statementsOfBindings(root, bindings) {
  return bindings.map(binding => {
    switch (binding.type) {
      case 'id':
        {
          // const <id> = <x>;
          const {
            key,
            kind,
            id
          } = binding;
          return (0, _Builders.variableDeclaration)(kind, id, expressionOfKey(root, key));
        }

      case 'array-rest':
        {
          // const <id> = <x>.slice(<exclude>);
          const {
            key,
            kind,
            id,
            exclude
          } = binding;
          const init = (0, _Builders.callExpression)({
            type: 'MemberExpression',
            object: expressionOfKey(root, key),
            property: (0, _Builders.ident)('slice'),
            computed: false,
            optional: false,
            ...(0, _Builders.etc)()
          }, [(0, _Builders.numberLiteral)(exclude)]);
          return (0, _Builders.variableDeclaration)(kind, id, init);
        }

      case 'object-rest':
        {
          // const {a: _, b: _, ...<id>} = <x>;
          const {
            key,
            kind,
            id,
            exclude
          } = binding;
          const destructuring = {
            type: 'ObjectPattern',
            properties: exclude.map(prop => prop.type === 'Identifier' ? {
              type: 'Property',
              key: (0, _astNodeMutationHelpers.shallowCloneNode)(prop),
              value: genIdent(),
              kind: 'init',
              computed: false,
              method: false,
              shorthand: false,
              ...(0, _Builders.etc)(),
              parent: _Builders.EMPTY_PARENT
            } : {
              type: 'Property',
              key: (0, _astNodeMutationHelpers.shallowCloneNode)(prop),
              value: genIdent(),
              kind: 'init',
              computed: true,
              method: false,
              shorthand: false,
              ...(0, _Builders.etc)(),
              parent: _Builders.EMPTY_PARENT
            }).concat({
              type: 'RestElement',
              argument: id,
              ...(0, _Builders.etc)()
            }),
            typeAnnotation: null,
            ...(0, _Builders.etc)()
          };
          return (0, _Builders.variableDeclaration)(kind, destructuring, expressionOfKey(root, key));
        }
    }
  });
}
/**
 * For throwing an error if no cases are matched.
 */


const fallthroughErrorMsgText = `Match: No case succesfully matched. Make exhaustive or add a wildcard case using '_'.`;

function fallthroughErrorMsg(value) {
  return {
    type: 'BinaryExpression',
    operator: '+',
    left: (0, _Builders.stringLiteral)(`${fallthroughErrorMsgText} Argument: `),
    right: value,
    ...(0, _Builders.etc)()
  };
}

function fallthroughError(value) {
  return (0, _Builders.throwStatement)(fallthroughErrorMsg(value));
}
/**
 * If the argument has no side-effects (ignoring getters). Either an identifier
 * or member expression with identifier root and non-computed/literal properties.
 */


function calculateSimpleArgument(node) {
  switch (node.type) {
    case 'Identifier':
    case 'Super':
      return true;

    case 'MemberExpression':
      {
        const {
          object,
          property,
          computed
        } = node;

        if (computed && property.type !== 'Literal') {
          return false;
        }

        return calculateSimpleArgument(object);
      }

    default:
      return false;
  }
}
/**
 * Analyze the match cases and return information we will use to build the result.
 */


function analyzeCases(cases) {
  let hasBindings = false;
  let hasWildcard = false;
  const analyses = [];

  for (let i = 0; i < cases.length; i++) {
    const {
      pattern,
      guard,
      body
    } = cases[i];
    const {
      conditions,
      bindings
    } = analyzePattern(pattern, [], new Set());
    hasBindings = hasBindings || bindings.length > 0;
    analyses.push({
      conditions,
      bindings,
      guard,
      body
    }); // This case catches everything, no reason to continue.

    if (conditions.length === 0 && guard == null) {
      hasWildcard = true;
      break;
    }
  }

  return {
    hasBindings,
    hasWildcard,
    analyses
  };
}
/**
 * Match expression transform entry point.
 */


function mapMatchExpression(node) {
  const {
    argument,
    cases
  } = node;
  const {
    hasBindings,
    hasWildcard,
    analyses
  } = analyzeCases(cases);
  const isSimpleArgument = !hasBindings && calculateSimpleArgument(argument);
  const genRoot = !isSimpleArgument ? genIdent() : null;
  const root = genRoot == null ? argument : genRoot; // No bindings and a simple argument means we can use nested conditional
  // expressions.

  if (isSimpleArgument) {
    const wildcardAnalaysis = hasWildcard ? analyses.pop() : null;
    const lastBody = wildcardAnalaysis != null ? wildcardAnalaysis.body : (0, _Builders.iife)([fallthroughError((0, _astNodeMutationHelpers.shallowCloneNode)(root))]);
    return analyses.reverse().reduce((acc, analysis) => {
      const {
        conditions,
        guard,
        body
      } = analysis;
      const tests = testsOfConditions(root, conditions);

      if (guard != null) {
        tests.push(guard);
      } // <tests> ? <body> : <acc>


      return {
        type: 'ConditionalExpression',
        test: (0, _Builders.conjunction)(tests),
        consequent: body,
        alternate: acc,
        ...(0, _Builders.etc)()
      };
    }, lastBody);
  } // There are bindings, so we produce an immediately invoked arrow expression.
  // If the original argument is simple, no need for a new variable.


  const statements = analyses.map(({
    conditions,
    bindings,
    guard,
    body
  }) => {
    const returnNode = {
      type: 'ReturnStatement',
      argument: body,
      ...(0, _Builders.etc)()
    }; // If we have a guard, then we use a nested if statement
    // `if (<guard>) return <body>`

    const bodyNode = guard == null ? returnNode : {
      type: 'IfStatement',
      test: guard,
      consequent: returnNode,
      ...(0, _Builders.etc)()
    };
    const bindingNodes = statementsOfBindings(root, bindings);
    const caseBody = bindingNodes.concat(bodyNode);

    if (conditions.length > 0) {
      const tests = testsOfConditions(root, conditions);
      return {
        type: 'IfStatement',
        test: (0, _Builders.conjunction)(tests),
        consequent: {
          type: 'BlockStatement',
          body: caseBody,
          ...(0, _Builders.etc)()
        },
        ...(0, _Builders.etc)()
      };
    } else {
      // No conditions, so no if statement
      if (bindingNodes.length > 0) {
        // Bindings require a block to introduce a new scope
        return {
          type: 'BlockStatement',
          body: caseBody,
          ...(0, _Builders.etc)()
        };
      } else {
        return bodyNode;
      }
    }
  });

  if (!hasWildcard) {
    statements.push(fallthroughError((0, _astNodeMutationHelpers.shallowCloneNode)(root)));
  }

  const [params, args] = genRoot == null ? [[], []] : [[genRoot], [argument]]; // `((<params>) => { ... })(<args>)`, or
  // `(() => { ... })()` if is simple argument.

  return (0, _Builders.iife)(statements, params, args);
}
/**
 * Match statement transform entry point.
 */


function mapMatchStatement(node) {
  const {
    argument,
    cases
  } = node;
  const {
    hasBindings,
    hasWildcard,
    analyses
  } = analyzeCases(cases);
  const topLabel = genIdent();
  const isSimpleArgument = !hasBindings && calculateSimpleArgument(argument);
  const genRoot = !isSimpleArgument ? genIdent() : null;
  const root = genRoot == null ? argument : genRoot;
  const statements = [];

  if (genRoot != null) {
    statements.push((0, _Builders.variableDeclaration)('const', genRoot, argument));
  }

  analyses.forEach(({
    conditions,
    bindings,
    guard,
    body
  }) => {
    const breakNode = {
      type: 'BreakStatement',
      label: (0, _astNodeMutationHelpers.shallowCloneNode)(topLabel),
      ...(0, _Builders.etc)()
    };
    const bodyStatements = body.body.concat(breakNode); // If we have a guard, then we use a nested if statement
    // `if (<guard>) return <body>`

    const guardedBodyStatements = guard == null ? bodyStatements : [{
      type: 'IfStatement',
      test: guard,
      consequent: {
        type: 'BlockStatement',
        body: bodyStatements,
        ...(0, _Builders.etc)()
      },
      ...(0, _Builders.etc)()
    }];
    const bindingNodes = statementsOfBindings(root, bindings);
    const caseBody = bindingNodes.concat(guardedBodyStatements);

    if (conditions.length > 0) {
      const tests = testsOfConditions(root, conditions);
      statements.push({
        type: 'IfStatement',
        test: (0, _Builders.conjunction)(tests),
        consequent: {
          type: 'BlockStatement',
          body: caseBody,
          ...(0, _Builders.etc)()
        },
        ...(0, _Builders.etc)()
      });
    } else {
      // No conditions, so no if statement
      statements.push({
        type: 'BlockStatement',
        body: caseBody,
        ...(0, _Builders.etc)()
      });
    }
  });

  if (!hasWildcard) {
    statements.push(fallthroughError((0, _astNodeMutationHelpers.shallowCloneNode)(root)));
  }

  return {
    type: 'LabeledStatement',
    label: topLabel,
    body: {
      type: 'BlockStatement',
      body: statements,
      ...(0, _Builders.etc)()
    },
    ...(0, _Builders.etc)()
  };
}

function transformProgram(program, _options) {
  // Initialize so each file transformed starts freshly incrementing the
  // variable name counter, and has its own usage tracking.
  GenID = (0, _GenID.createGenID)('m');
  return _SimpleTransform.SimpleTransform.transformProgram(program, {
    transform(node) {
      switch (node.type) {
        case 'MatchExpression':
          {
            return mapMatchExpression(node);
          }

        case 'MatchStatement':
          {
            return mapMatchStatement(node);
          }

        case 'Identifier':
          {
            // A rudimentary check to avoid some collisions with our generated
            // variable names. Ideally, we would have access a scope analyzer
            // inside the transform instead.
            if (GenID == null) {
              throw Error('GenID must be initialized at the start of the transform.');
            }

            GenID.addUsage(node.name);
            return node;
          }

        default:
          {
            return node;
          }
      }
    }

  });
}