"use strict";

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

var _HermesASTAdapter = _interopRequireDefault(require("./HermesASTAdapter"));

var _getModuleDocblock = require("./getModuleDocblock");

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

/**
 * 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
 */

/*
This class does some very "javascripty" things in the name of
performance which are ultimately impossible to soundly type.

So instead of adding strict types and a large number of suppression
comments, instead it is left untyped and subclasses are strictly
typed via a separate flow declaration file.
*/
class HermesToESTreeAdapter extends _HermesASTAdapter.default {
  constructor(options, code) {
    super(options);
    this.code = void 0;
    this.code = code;
  }

  fixSourceLocation(node) {
    var _this$sourceFilename;

    const loc = node.loc;

    if (loc == null) {
      return;
    }

    node.loc = {
      source: (_this$sourceFilename = this.sourceFilename) != null ? _this$sourceFilename : null,
      start: loc.start,
      end: loc.end
    };
    node.range = [loc.rangeStart, loc.rangeEnd];
    delete node.start;
    delete node.end;
  }

  mapNode(node) {
    this.fixSourceLocation(node);

    switch (node.type) {
      case 'Program':
        return this.mapProgram(node);

      case 'NullLiteral':
        return this.mapNullLiteral(node);

      case 'BooleanLiteral':
      case 'StringLiteral':
      case 'NumericLiteral':
      case 'JSXStringLiteral':
        return this.mapSimpleLiteral(node);

      case 'BigIntLiteral':
        return this.mapBigIntLiteral(node);

      case 'RegExpLiteral':
        return this.mapRegExpLiteral(node);

      case 'Empty':
        return this.mapEmpty(node);

      case 'TemplateElement':
        return this.mapTemplateElement(node);

      case 'BigIntLiteralTypeAnnotation':
        return this.mapBigIntLiteralTypeAnnotation(node);

      case 'GenericTypeAnnotation':
        return this.mapGenericTypeAnnotation(node);

      case 'ImportDeclaration':
        return this.mapImportDeclaration(node);

      case 'ImportSpecifier':
        return this.mapImportSpecifier(node);

      case 'ExportDefaultDeclaration':
        return this.mapExportDefaultDeclaration(node);

      case 'ExportNamedDeclaration':
        return this.mapExportNamedDeclaration(node);

      case 'ExportAllDeclaration':
        return this.mapExportAllDeclaration(node);

      case 'Property':
        return this.mapProperty(node);

      case 'FunctionDeclaration':
      case 'FunctionExpression':
      case 'ArrowFunctionExpression':
        return this.mapFunction(node);

      case 'PrivateName':
        return this.mapPrivateName(node);

      case 'ClassProperty':
      case 'ClassPrivateProperty':
        return this.mapClassProperty(node);

      case 'MemberExpression':
      case 'OptionalMemberExpression':
      case 'CallExpression':
      case 'OptionalCallExpression':
        return this.mapChainExpression(node);

      default:
        return this.mapNodeDefault(node);
    }
  }

  mapProgram(node) {
    node = this.mapNodeDefault(node);
    node.sourceType = this.getSourceType();
    node.docblock = (0, _getModuleDocblock.getModuleDocblock)(node);
    return node;
  }

  mapSimpleLiteral(node) {
    return {
      type: 'Literal',
      loc: node.loc,
      range: node.range,
      value: node.value,
      raw: this.code.slice(node.range[0], node.range[1]),
      literalType: (() => {
        switch (node.type) {
          case 'NullLiteral':
            return 'null';

          case 'BooleanLiteral':
            return 'boolean';

          case 'StringLiteral':
          case 'JSXStringLiteral':
            return 'string';

          case 'NumericLiteral':
            return 'numeric';

          case 'BigIntLiteral':
            return 'bigint';

          case 'RegExpLiteral':
            return 'regexp';
        }

        return null;
      })()
    };
  }

  mapBigIntLiteral(node) {
    const newNode = this.mapSimpleLiteral(node);
    const bigint = node.bigint // estree spec is to not have a trailing `n` on this property
    // https://github.com/estree/estree/blob/db962bb417a97effcfe9892f87fbb93c81a68584/es2020.md#bigintliteral
    .replace(/n$/, '') // `BigInt` doesn't accept numeric separator and `bigint` property should not include numeric separator
    .replace(/_/, '');
    return { ...newNode,
      // coerce the string to a bigint value if supported by the environment
      value: typeof BigInt === 'function' ? BigInt(bigint) : null,
      bigint
    };
  }

  mapNullLiteral(node) {
    return { ...this.mapSimpleLiteral(node),
      value: null
    };
  }

  mapRegExpLiteral(node) {
    const {
      pattern,
      flags
    } = node; // Create RegExp value if possible. This can fail when the flags are invalid.

    let value;

    try {
      value = new RegExp(pattern, flags);
    } catch (e) {
      value = null;
    }

    return { ...this.mapSimpleLiteral(node),
      value,
      regex: {
        pattern,
        flags
      }
    };
  }

  mapBigIntLiteralTypeAnnotation(node) {
    node.value = null;
    return node;
  }

  mapTemplateElement(node) {
    return {
      type: 'TemplateElement',
      loc: node.loc,
      range: node.range,
      tail: node.tail,
      value: {
        cooked: node.cooked,
        raw: node.raw
      }
    };
  }

  mapGenericTypeAnnotation(node) {
    // Convert simple `this` generic type to ThisTypeAnnotation
    if (node.typeParameters == null && node.id.type === 'Identifier' && node.id.name === 'this') {
      return {
        type: 'ThisTypeAnnotation',
        loc: node.loc,
        range: node.range
      };
    }

    return this.mapNodeDefault(node);
  }

  mapProperty(nodeUnprocessed) {
    const node = this.mapNodeDefault(nodeUnprocessed);

    if (node.value.type === 'FunctionExpression') {
      node.value.loc.start = node.key.loc.end;
      node.value.range[0] = node.key.range[1];
    }

    return node;
  }

  mapComment(node) {
    if (node.type === 'CommentBlock') {
      node.type = 'Block';
    } else if (node.type === 'CommentLine') {
      node.type = 'Line';
    }

    return node;
  }

  mapFunction(nodeUnprocessed) {
    const node = this.mapNodeDefault(nodeUnprocessed);

    switch (node.type) {
      case 'FunctionDeclaration':
      case 'FunctionExpression':
        node.expression = false;
        return node;

      case 'ArrowFunctionExpression':
        node.expression = node.body.type !== 'BlockStatement';
        return node;
    }

    return node;
  }

  mapChainExpression(nodeUnprocessed) {
    /*
    NOTE - In the below comments `MemberExpression` and `CallExpression`
           are completely interchangable. For terseness we just reference
           one each time.
    */

    /*
    Hermes uses the old babel-style AST:
    ```
    (one?.two).three?.four;
    ^^^^^^^^^^^^^^^^^^^^^^ OptionalMemberExpression
    ^^^^^^^^^^^^^^^^ MemberExpression
     ^^^^^^^^ OptionalMemberExpression
    ```
     We need to convert it to the ESTree representation:
    ```
    (one?.two).three?.four;
    ^^^^^^^^^^^^^^^^^^^^^^ ChainExpression
    ^^^^^^^^^^^^^^^^^^^^^^ MemberExpression[optional = true]
    ^^^^^^^^^^^^^^^^ MemberExpression[optional = false]
     ^^^^^^^^ ChainExpression
     ^^^^^^^^ MemberExpression[optional = true]
    ```
     We do this by converting the AST and its children (depth first), and then unwrapping
    the resulting AST as appropriate.
     Put another way:
    1) traverse to the leaf
    2) if the current node is an `OptionalMemberExpression`:
      a) if the `.object` is a `ChainExpression`:
        i)   unwrap the child (`node.object = child.expression`)
      b) convert this node to a `MemberExpression[optional = true]`
      c) wrap this node (`node = ChainExpression[expression = node]`)
    3) if the current node is a `MembedExpression`:
      a) convert this node to a `MemberExpression[optional = true]`
    */
    const node = this.mapNodeDefault(nodeUnprocessed);

    const {
      child,
      childKey,
      isOptional
    } = (() => {
      const isOptional = node.optional === true;

      if (node.type.endsWith('MemberExpression')) {
        return {
          child: node.object,
          childKey: 'object',
          isOptional
        };
      } else if (node.type.endsWith('CallExpression')) {
        return {
          child: node.callee,
          childKey: 'callee',
          isOptional
        };
      } else {
        return {
          child: node.expression,
          childKey: 'expression',
          isOptional: false
        };
      }
    })();

    const isChildUnwrappable = child.type === 'ChainExpression' && // (x?.y).z is semantically different to `x?.y.z`.
    // In the un-parenthesised case `.z` is only executed if and only if `x?.y` returns a non-nullish value.
    // In the parenthesised case, `.z` is **always** executed, regardless of the return of `x?.y`.
    // As such the AST is different between the two cases.
    //
    // In the hermes AST - any member part of a non-short-circuited optional chain is represented with `OptionalMemberExpression`
    // so if we see a `MemberExpression`, then we know we've hit a parenthesis boundary.
    node.type !== 'MemberExpression' && node.type !== 'CallExpression';

    if (node.type.startsWith('Optional')) {
      node.type = node.type.replace('Optional', '');
      node.optional = isOptional;
    } else {
      node.optional = false;
    }

    if (!isChildUnwrappable && !isOptional) {
      return node;
    }

    if (isChildUnwrappable) {
      const newChild = child.expression;
      node[childKey] = newChild;
    }

    return {
      type: 'ChainExpression',
      expression: node,
      loc: node.loc,
      range: node.range
    };
  }

  mapClassProperty(nodeUnprocessed) {
    const node = this.mapNodeDefault(nodeUnprocessed);

    const key = (() => {
      if (node.type === 'ClassPrivateProperty') {
        const key = this.mapNodeDefault(node.key);
        return {
          type: 'PrivateIdentifier',
          name: key.name,
          range: key.range,
          loc: key.loc
        };
      }

      return node.key;
    })();

    return { ...node,
      computed: node.type === 'ClassPrivateProperty' ? false : node.computed,
      key,
      type: 'PropertyDefinition'
    };
  }

  mapPrivateName(node) {
    return {
      type: 'PrivateIdentifier',
      name: node.id.name,
      // estree the location refers to the entire string including the hash token
      range: node.range,
      loc: node.loc
    };
  }

  mapExportNamedDeclaration(nodeUnprocessed) {
    const node = super.mapExportNamedDeclaration(nodeUnprocessed);
    const namespaceSpecifier = node.specifiers.find(spec => spec.type === 'ExportNamespaceSpecifier');

    if (namespaceSpecifier != null) {
      var _node$exportKind;

      if (node.specifiers.length !== 1) {
        // this should already a hermes parser error - but let's be absolutely sure we're aligned with the spec
        throw new Error('Cannot use an export all with any other specifiers');
      }

      return {
        type: 'ExportAllDeclaration',
        source: node.source,
        exportKind: (_node$exportKind = node.exportKind) != null ? _node$exportKind : 'value',
        exported: namespaceSpecifier.exported,
        range: node.range,
        loc: node.loc
      };
    }

    return node;
  }

  mapExportAllDeclaration(nodeUnprocessed) {
    var _node$exported;

    const node = super.mapExportAllDeclaration(nodeUnprocessed);
    node.exported = (_node$exported = node.exported) != null ? _node$exported : null;
    return node;
  }

}

exports.default = HermesToESTreeAdapter;