/*
 * Copyright 2018 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.BooleanLiteralSet;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import com.google.javascript.rhino.jstype.TemplateTypeMap;
import com.google.javascript.rhino.jstype.TemplateTypeMapReplacer;
import javax.annotation.Nullable;

/**
 * Creates AST nodes and subtrees.
 *
 * <p>This class supports creating nodes either with or without type information.
 *
 * <p>The idea is that client code can create the trees of nodes it needs without having to contain
 * logic for deciding whether type information should be added or not, and only minimal logic for
 * determining which types to add when they are necessary. Most methods in this class are able to
 * determine the correct type information from already existing AST nodes and the current scope.
 */
final class AstFactory {

  @Nullable private final JSTypeRegistry registry;
  // We need the unknown type so frequently, it's worth caching it.
  private final JSType unknownType;
  // We might not need Arguments type, but if we do, we should avoid redundant lookups
  private final Supplier<JSType> argumentsTypeSupplier;

  private AstFactory() {
    this.registry = null;
    unknownType = null;
    argumentsTypeSupplier = () -> null;
  }

  private AstFactory(JSTypeRegistry registry) {
    this.registry = registry;
    this.unknownType = getNativeType(JSTypeNative.UNKNOWN_TYPE);
    this.argumentsTypeSupplier =
        Suppliers.memoize(
            () -> {
              JSType globalType = registry.getGlobalType("Arguments");
              if (globalType != null) {
                return globalType;
              } else {
                return unknownType;
              }
            });
    ;
  }

  static AstFactory createFactoryWithoutTypes() {
    return new AstFactory();
  }

  static AstFactory createFactoryWithTypes(JSTypeRegistry registry) {
    return new AstFactory(registry);
  }

  /** Does this class instance add types to the nodes it creates? */
  boolean isAddingTypes() {
    return registry != null;
  }

  /**
   * Returns a new EXPR_RESULT node.
   *
   * <p>Statements have no type information, so this is functionally the same as calling {@code
   * IR.exprResult(expr)}. It exists so that a pass can be consistent about always using {@code
   * AstFactory} to create new nodes.
   */
  Node exprResult(Node expr) {
    return IR.exprResult(expr);
  }

  /**
   * Returns a new EMPTY node.
   *
   * <p>EMPTY Nodes have no type information, so this is functionally the same as calling {@code
   * IR.empty()}. It exists so that a pass can be consistent about always using {@code AstFactory}
   * to create new nodes.
   */
  Node createEmpty() {
    return IR.empty();
  }

  /**
   * Returns a new BLOCK node.
   *
   * <p>Blocks have no type information, so this is functionally the same as calling {@code
   * IR.block(statements)}. It exists so that a pass can be consistent about always using {@code
   * AstFactory} to create new nodes.
   */
  Node createBlock(Node... statements) {
    return IR.block(statements);
  }

  /**
   * Returns a new IF node.
   *
   * <p>Blocks have no type information, so this is functionally the same as calling {@code
   * IR.ifNode(cond, then)}. It exists so that a pass can be consistent about always using {@code
   * AstFactory} to create new nodes.
   */
  Node createIf(Node cond, Node then) {
    return IR.ifNode(cond, then);
  }

  /**
   * Returns a new IF node.
   *
   * <p>Blocks have no type information, so this is functionally the same as calling {@code
   * IR.ifNode(cond, then, elseNode)}. It exists so that a pass can be consistent about always using
   * {@code AstFactory} to create new nodes.
   */
  Node createIf(Node cond, Node then, Node elseNode) {
    return IR.ifNode(cond, then, elseNode);
  }

  /**
   * Returns a new FOR node.
   *
   * <p>Blocks have no type information, so this is functionally the same as calling {@code
   * IR.forNode(init, cond, incr, body)}. It exists so that a pass can be consistent about always
   * using {@code AstFactory} to create new nodes.
   */
  Node createFor(Node init, Node cond, Node incr, Node body) {
    return IR.forNode(init, cond, incr, body);
  }

  /**
   * Returns a new BREAK node.
   *
   * <p>Breaks have no type information, so this is functionally the same as calling {@code
   * IR.breakNode()}. It exists so that a pass can be consistent about always using {@code
   * AstFactory} to create new nodes.
   */
  Node createBreak() {
    return IR.breakNode();
  }

  /**
   * Returns a new {@code return} statement.
   *
   * <p>Return statements have no type information, so this is functionally the same as calling
   * {@code IR.return(value)}. It exists so that a pass can be consistent about always using {@code
   * AstFactory} to create new nodes.
   */
  Node createReturn(Node value) {
    return IR.returnNode(value);
  }

  /**
   * Returns a new {@code yield} expression.
   *
   * @param jsType Type we expect to get back after the yield
   * @param value value to yield
   */
  Node createYield(JSType jsType, Node value) {
    Node result = IR.yield(value);
    if (isAddingTypes()) {
      result.setJSType(checkNotNull(jsType));
    }
    return result;
  }

  /**
   * Returns a new {@code await} expression.
   *
   * @param jsType Type we expect to get back after the await
   * @param value value to await
   */
  Node createAwait(JSType jsType, Node value) {
    Node result = IR.await(value);
    if (isAddingTypes()) {
      result.setJSType(checkNotNull(jsType));
    }
    return result;
  }

  Node createString(String value) {
    Node result = IR.string(value);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.STRING_TYPE));
    }
    return result;
  }

  Node createNumber(double value) {
    Node result = IR.number(value);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.NUMBER_TYPE));
    }
    return result;
  }

  Node createBoolean(boolean value) {
    Node result = value ? IR.trueNode() : IR.falseNode();
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
    }
    return result;
  }

  Node createNull() {
    Node result = IR.nullNode();
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.NULL_TYPE));
    }
    return result;
  }

  Node createThis(JSType thisType) {
    Node result = IR.thisNode();
    if (isAddingTypes()) {
      result.setJSType(checkNotNull(thisType));
    }
    return result;
  }

  Node createSuper(JSType superType) {
    Node result = IR.superNode();
    if (isAddingTypes()) {
      result.setJSType(checkNotNull(superType));
    }
    return result;
  }

  /** Creates a THIS node with the correct type for the given function node. */
  Node createThisForFunction(Node functionNode) {
    final Node result = IR.thisNode();
    if (isAddingTypes()) {
      result.setJSType(getTypeOfThisForFunctionNode(functionNode));
    }
    return result;
  }

  /** Creates a SUPER node with the correct type for the given function node. */
  Node createSuperForFunction(Node functionNode) {
    final Node result = IR.superNode();
    if (isAddingTypes()) {
      result.setJSType(getTypeOfSuperForFunctionNode(functionNode));
    }
    return result;
  }

  @Nullable
  private JSType getTypeOfThisForFunctionNode(Node functionNode) {
    if (isAddingTypes()) {
      FunctionType functionType = getFunctionType(functionNode);
      return checkNotNull(functionType.getTypeOfThis(), functionType);
    } else {
      return null; // not adding type information
    }
  }

  @Nullable
  private JSType getTypeOfSuperForFunctionNode(Node functionNode) {
    if (isAddingTypes()) {
      ObjectType thisType = getTypeOfThisForFunctionNode(functionNode).assertObjectType();
      return checkNotNull(thisType.getSuperClassConstructor().getInstanceType(), thisType);
    } else {
      return null; // not adding type information
    }
  }

  private FunctionType getFunctionType(Node functionNode) {
    checkState(functionNode.isFunction(), "not a function: %s", functionNode);
    // If the function declaration was cast to a different type, we want the original type
    // from before the cast.
    final JSType typeBeforeCast = functionNode.getJSTypeBeforeCast();
    final FunctionType functionType;
    if (typeBeforeCast != null) {
      functionType = typeBeforeCast.assertFunctionType();
    } else {
      functionType = functionNode.getJSTypeRequired().assertFunctionType();
    }
    return functionType;
  }

  /** Creates a NAME node having the type of "this" appropriate for the given function node. */
  Node createThisAliasReferenceForFunction(String aliasName, Node functionNode) {
    final Node result = IR.name(aliasName);
    if (isAddingTypes()) {
      result.setJSType(getTypeOfThisForFunctionNode(functionNode));
    }
    return result;
  }

  /**
   * Creates a statement declaring a const alias for "this" to be used in the given function node.
   *
   * <p>e.g. `const aliasName = this;`
   */
  Node createThisAliasDeclarationForFunction(String aliasName, Node functionNode) {
    return createSingleConstNameDeclaration(
        aliasName, createThis(getTypeOfThisForFunctionNode(functionNode)));
  }

  /**
   * Creates a new `const` declaration statement for a single variable name.
   *
   * <p>Takes the type for the variable name from the value node.
   *
   * <p>e.g. `const variableName = value;`
   */
  Node createSingleConstNameDeclaration(String variableName, Node value) {
    return IR.constNode(createName(variableName, value.getJSType()), value);
  }

  /**
   * Creates a reference to "arguments" with the type specified in externs, or unknown if the
   * externs for it weren't included.
   */
  Node createArgumentsReference() {
    Node result = IR.name("arguments");
    if (isAddingTypes()) {
      result.setJSType(argumentsTypeSupplier.get());
    }
    return result;
  }

  /**
   * Creates a statement declaring a const alias for "arguments".
   *
   * <p>e.g. `const argsAlias = arguments;`
   */
  Node createArgumentsAliasDeclaration(String aliasName) {
    return createSingleConstNameDeclaration(aliasName, createArgumentsReference());
  }

  Node createName(String name, JSType type) {
    Node result = IR.name(name);
    if (isAddingTypes()) {
      result.setJSType(checkNotNull(type));
    }
    return result;
  }

  Node createName(String name, JSTypeNative nativeType) {
    Node result = IR.name(name);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(nativeType));
    }
    return result;
  }

  Node createNameWithUnknownType(String name) {
    return createName(name, unknownType);
  }

  Node createName(Scope scope, String name) {
    Node result = IR.name(name);
    if (isAddingTypes()) {
      result.setJSType(getVarNameType(scope, name));
    }
    return result;
  }

  Node createQName(Scope scope, String qname) {
    return createQName(scope, Splitter.on(".").split(qname));
  }

  private Node createQName(Scope scope, Iterable<String> names) {
    String baseName = checkNotNull(Iterables.getFirst(names, null));
    Iterable<String> propertyNames = Iterables.skip(names, 1);
    Node baseNameNode = createName(scope, baseName);
    return createGetProps(baseNameNode, propertyNames);
  }

  Node createGetProp(Node receiver, String propertyName) {
    Node result = IR.getprop(receiver, IR.string(propertyName));
    if (isAddingTypes()) {
      result.setJSType(getJsTypeForProperty(receiver, propertyName));
    }
    return result;
  }

  /** Creates a tree of nodes representing `receiver.name1.name2.etc`. */
  private Node createGetProps(Node receiver, Iterable<String> propertyNames) {
    Node result = receiver;
    for (String propertyName : propertyNames) {
      result = createGetProp(result, propertyName);
    }
    return result;
  }

  /** Creates a tree of nodes representing `receiver.name1.name2.etc`. */
  Node createGetProps(Node receiver, String firstPropName, String... otherPropNames) {
    Node result = createGetProp(receiver, firstPropName);
    for (String propertyName : otherPropNames) {
      result = createGetProp(result, propertyName);
    }
    return result;
  }

  Node createGetElem(Node receiver, Node key) {
    Node result = IR.getelem(receiver, key);
    if (isAddingTypes()) {
      // In general we cannot assume we know the type we get from a GETELEM.
      // TODO(bradfordcsmith): When receiver is an Array<T> or an Object<K, V>, use the template
      // type here.
      result.setJSType(unknownType);
    }
    return result;
  }

  Node createDelProp(Node target) {
    Node result = IR.delprop(target);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
    }
    return result;
  }

  Node createStringKey(String key, Node value) {
    Node result = IR.stringKey(key, value);
    if (isAddingTypes()) {
      result.setJSType(value.getJSType());
    }
    return result;
  }

  Node createComputedProperty(Node key, Node value) {
    Node result = IR.computedProp(key, value);
    if (isAddingTypes()) {
      result.setJSType(value.getJSType());
    }
    return result;
  }

  Node createIn(Node left, Node right) {
    Node result = IR.in(left, right);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
    }
    return result;
  }

  Node createComma(Node left, Node right) {
    Node result = IR.comma(left, right);
    if (isAddingTypes()) {
      result.setJSType(right.getJSType());
    }
    return result;
  }

  Node createCommas(Node first, Node second, Node... rest) {
    Node result = createComma(first, second);
    for (Node next : rest) {
      result = createComma(result, next);
    }
    return result;
  }

  Node createAnd(Node left, Node right) {
    Node result = IR.and(left, right);
    if (isAddingTypes()) {
      JSType leftType = checkNotNull(left.getJSType(), left);
      JSType rightType = checkNotNull(right.getJSType(), right);

      BooleanLiteralSet possibleLhsBooleanValues = leftType.getPossibleToBooleanOutcomes();
      switch (possibleLhsBooleanValues) {
        case TRUE:
          // left cannot be false, so rhs will always be evaluated
          result.setJSType(rightType);
          break;
        case FALSE:
          // left cannot be true, so rhs will never be evaluated
          result.setJSType(leftType);
          break;
        case BOTH:
          // result could be the type of either the lhs or the rhs
          result.setJSType(leftType.getLeastSupertype(rightType));
          break;
        default:
          checkState(
              possibleLhsBooleanValues == BooleanLiteralSet.EMPTY,
              "unexpected enum value: %s",
              possibleLhsBooleanValues);
          // TODO(bradfordcsmith): Should we be trying to determine whether we actually need
          // NO_OBJECT_TYPE or similar here? It probably doesn't matter since this code is
          // expected to execute only after all static type analysis has been done.
          result.setJSType(getNativeType(JSTypeNative.NO_TYPE));
          break;
      }
    }
    return result;
  }

  Node createOr(Node left, Node right) {
    Node result = IR.or(left, right);
    if (isAddingTypes()) {
      JSType leftType = checkNotNull(left.getJSType(), left);
      JSType rightType = checkNotNull(right.getJSType(), right);

      BooleanLiteralSet possibleLhsBooleanValues = leftType.getPossibleToBooleanOutcomes();
      switch (possibleLhsBooleanValues) {
        case TRUE:
          // left cannot be false, so rhs will never be evaluated
          result.setJSType(leftType);
          break;
        case FALSE:
          // left cannot be true, so rhs will always be evaluated
          result.setJSType(rightType);
          break;
        case BOTH:
          // result could be the type of either the lhs or the rhs
          result.setJSType(leftType.getLeastSupertype(rightType));
          break;
        default:
          checkState(
              possibleLhsBooleanValues == BooleanLiteralSet.EMPTY,
              "unexpected enum value: %s",
              possibleLhsBooleanValues);
          // TODO(bradfordcsmith): Should we be trying to determine whether we actually need
          // NO_OBJECT_TYPE or similar here? It probably doesn't matter since this code is
          // expected to execute only after all static type analysis has been done.
          result.setJSType(getNativeType(JSTypeNative.NO_TYPE));
          break;
      }
    }
    return result;
  }

  Node createCall(Node callee, Node... args) {
    Node result = NodeUtil.newCallNode(callee, args);
    if (isAddingTypes()) {
      FunctionType calleeType = JSType.toMaybeFunctionType(callee.getJSType());
      // TODO(sdh): this does not handle generic functions - we'd need to unify the argument types.
      // checkState(calleeType == null || !calleeType.hasAnyTemplateTypes(), calleeType);
      // TODO(bradfordcsmith): Consider throwing an exception if calleeType is null.
      JSType returnType = calleeType != null ? calleeType.getReturnType() : unknownType;
      result.setJSType(returnType);
    }
    return result;
  }

  /**
   * Creates a call to Object.assign that returns the specified type.
   *
   * <p>Object.assign returns !Object in the externs, which can lose type information if the actual
   * type is known.
   */
  Node createObjectDotAssignCall(Scope scope, JSType returnType, Node... args) {
    Node objAssign = createQName(scope, "Object.assign");
    Node result = createCall(objAssign, args);

    if (isAddingTypes()) {
      // Make a unique function type that returns the exact type we've inferred it to be.
      // Object.assign in the externs just returns !Object, which loses type information.
      JSType objAssignType =
          registry.createFunctionTypeWithVarArgs(
              returnType,
              registry.getNativeType(JSTypeNative.OBJECT_TYPE),
              registry.createUnionType(JSTypeNative.OBJECT_TYPE, JSTypeNative.NULL_TYPE));
      objAssign.setJSType(objAssignType);
      result.setJSType(returnType);
    }

    return result;
  }

  Node createNewNode(Node target, Node... args) {
    Node result = IR.newNode(target, args);
    if (isAddingTypes()) {
      JSType instanceType = target.getJSType();
      if (instanceType.isFunctionType()) {
        instanceType = instanceType.toMaybeFunctionType().getInstanceType();
      } else {
        instanceType = getNativeType(JSTypeNative.UNKNOWN_TYPE);
      }
      result.setJSType(instanceType);
    }
    return result;
  }

  Node createObjectGetPrototypeOfCall(Node argObjectNode) {
    Node objectName = createName("Object", JSTypeNative.OBJECT_FUNCTION_TYPE);
    Node objectGetPrototypeOf = createGetProp(objectName, "getPrototypeOf");
    Node result = createCall(objectGetPrototypeOf, argObjectNode);
    if (isAddingTypes()) {
      ObjectType typeOfArgObject = argObjectNode.getJSTypeRequired().assertObjectType();
      JSType returnedType = getPrototypeObjectType(typeOfArgObject);
      result.setJSType(returnedType);

      // Return type of the function needs to match that of the entire expression. getPrototypeOf
      // normally returns !Object.
      objectGetPrototypeOf.setJSType(
          registry.createFunctionType(returnedType, getNativeType(JSTypeNative.OBJECT_TYPE)));
    }
    return result;
  }

  ObjectType getPrototypeObjectType(ObjectType objectType) {
    checkNotNull(objectType);
    if (objectType.isUnknownType()) {
      // Calling getImplicitPrototype() on the unknown type returns `null`, but we want
      // the prototype of an unknown type to also be unknown.
      // TODO(bradfordcsmith): Can we fix this behavior of the unknown type?
      return objectType;
    } else {
      return checkNotNull(objectType.getImplicitPrototype(), "null prototype: %s", objectType);
    }
  }

  /**
   * Create a call that returns an instance of the given class type.
   *
   * <p>This method is intended for use in special cases, such as calling `super()` in a
   * constructor.
   */
  Node createConstructorCall(@Nullable JSType classType, Node callee, Node... args) {
    Node result = NodeUtil.newCallNode(callee, args);
    if (isAddingTypes()) {
      checkNotNull(classType);
      FunctionType constructorType = checkNotNull(classType.toMaybeFunctionType());
      ObjectType instanceType = checkNotNull(constructorType.getInstanceType());
      result.setJSType(instanceType);
    }
    return result;
  }

  /** Creates a statement `lhs = rhs;`. */
  Node createAssignStatement(Node lhs, Node rhs) {
    return exprResult(createAssign(lhs, rhs));
  }

  /** Creates an assignment expression `lhs = rhs` */
  Node createAssign(Node lhs, Node rhs) {
    Node result = IR.assign(lhs, rhs);
    if (isAddingTypes()) {
      result.setJSType(rhs.getJSType());
    }
    return result;
  }

  /**
   * Creates an empty object literal, `{}`.
   *
   * <p>TODO(nickreid): Consider a single method {@code createObjectLit}, which accepts varargs.
   * When this method was created it seemed valuable to explicitly distinguish these cases, which is
   * why this method cannot be called with args. However, that differentiation might be frustrating
   * to callers.
   */
  Node createEmptyObjectLit() {
    Node result = IR.objectlit();
    if (isAddingTypes()) {
      result.setJSType(registry.createAnonymousObjectType(null));
    }
    return result;
  }

  /** Creates an empty function `function() {}` */
  Node createEmptyFunction(JSType type) {
    Node result = NodeUtil.emptyFunction();
    if (isAddingTypes()) {
      checkNotNull(type);
      checkArgument(type.isFunctionType(), type);
      result.setJSType(checkNotNull(type));
    }
    return result;
  }

  /** Creates an empty function `function*() {}` */
  Node createEmptyGeneratorFunction(JSType type) {
    Node result = NodeUtil.emptyFunction();
    result.setIsGeneratorFunction(true);
    if (isAddingTypes()) {
      checkNotNull(type);
      checkArgument(type.isFunctionType(), type);
      result.setJSType(checkNotNull(type));
    }
    return result;
  }

  /**
   * Creates a function `function name(paramList) { body }`
   *
   * @param name STRING node - empty string if no name
   * @param paramList PARAM_LIST node
   * @param body BLOCK node
   * @param type type to apply to the function itself
   */
  Node createFunction(String name, Node paramList, Node body, JSType type) {
    Node nameNode = createName(name, type);
    Node result = IR.function(nameNode, paramList, body);
    if (isAddingTypes()) {
      checkArgument(type.isFunctionType(), type);
      result.setJSType(type);
    }
    return result;
  }

  Node createZeroArgFunction(String name, Node body, @Nullable JSType returnType) {
    FunctionType functionType = isAddingTypes() ? registry.createFunctionType(returnType) : null;
    return createFunction(name, IR.paramList(), body, functionType);
  }

  Node createZeroArgGeneratorFunction(String name, Node body, @Nullable JSType returnType) {
    Node result = createZeroArgFunction(name, body, returnType);
    result.setIsGeneratorFunction(true);
    return result;
  }

  Node createZeroArgArrowFunctionForExpression(Node expression) {
    Node result = IR.arrowFunction(IR.name(""), IR.paramList(), expression);
    if (isAddingTypes()) {
      // It feels like we should be adding type-of-this here, but it should remain unknown,
      // because you're allowed to supply any kind of value of `this` when calling an arrow
      // function. It will just be ignored in favor of the `this` in the scope where the
      // arrow was defined.
      FunctionType functionType =
          FunctionType.builder(registry)
              .withReturnType(expression.getJSTypeRequired())
              .withParamsNode(IR.paramList())
              .build();
      result.setJSType(functionType);
    }
    return result;
  }

  Node createMemberFunctionDef(String name, Node function) {
    // A function used for a member function definition must have an empty name,
    // because the name string goes on the MEMBER_FUNCTION_DEF node.
    checkArgument(function.getFirstChild().getString().isEmpty(), function);
    Node result = IR.memberFunctionDef(name, function);
    if (isAddingTypes()) {
      // member function definition must share the type of the function that implements it
      result.setJSType(function.getJSType());
    }
    return result;
  }

  Node createSheq(Node expr1, Node expr2) {
    Node result = IR.sheq(expr1, expr2);
    if (isAddingTypes()) {
      result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
    }
    return result;
  }

  Node createHook(Node condition, Node expr1, Node expr2) {
    Node result = IR.hook(condition, expr1, expr2);
    if (isAddingTypes()) {
      result.setJSType(registry.createUnionType(expr1.getJSType(), expr2.getJSType()));
    }
    return result;
  }

  Node createArraylit(Node... elements) {
    Node result = IR.arraylit(elements);
    if (isAddingTypes()) {
      result.setJSType(
          registry.createTemplatizedType(
              registry.getNativeObjectType(JSTypeNative.ARRAY_TYPE),
              // TODO(nickreid): Use a reasonable template type. Remeber to consider SPREAD.
              getNativeType(JSTypeNative.UNKNOWN_TYPE)));
    }
    return result;
  }

  Node createJSCompMakeIteratorCall(Node iterable, Scope scope) {
    String function = "makeIterator";
    Node makeIteratorName = createQName(scope, "$jscomp." + function);
    // Since createCall (currently) doesn't handle templated functions, fill in the template types
    // of makeIteratorName manually.
    if (isAddingTypes() && !makeIteratorName.getJSType().isUnknownType()) {
      // if makeIteratorName has the unknown type, we must have not injected the required runtime
      // libraries - hopefully because this is in a test using NonInjectingCompiler.

      // e.g get `number` from `Iterable<number>`
      JSType iterableType =
          iterable
              .getJSType()
              .getInstantiatedTypeArgument(getNativeType(JSTypeNative.ITERABLE_TYPE));
      JSType makeIteratorType = makeIteratorName.getJSType();
      // e.g. replace
      //   function(Iterable<T>): Iterator<T>
      // with
      //   function(Iterable<number>): Iterator<number>
      TemplateTypeMap typeMap =
          registry.createTemplateTypeMap(
              makeIteratorType.getTemplateTypeMap().getTemplateKeys(),
              ImmutableList.of(iterableType));
      TemplateTypeMapReplacer replacer = new TemplateTypeMapReplacer(registry, typeMap);
      makeIteratorName.setJSType(makeIteratorType.visit(replacer));
    }
    return createCall(makeIteratorName, iterable);
  }

  Node createJscompArrayFromIteratorCall(Node iterator, Scope scope) {
    String function = "arrayFromIterator";
    Node makeIteratorName = createQName(scope, "$jscomp." + function);
    // Since createCall (currently) doesn't handle templated functions, fill in the template types
    // of makeIteratorName manually.
    if (isAddingTypes() && !makeIteratorName.getJSType().isUnknownType()) {
      // if makeIteratorName has the unknown type, we must have not injected the required runtime
      // libraries - hopefully because this is in a test using NonInjectingCompiler.

      // e.g get `number` from `Iterator<number>`
      JSType iterableType =
          iterator
              .getJSType()
              .getInstantiatedTypeArgument(getNativeType(JSTypeNative.ITERATOR_TYPE));
      JSType makeIteratorType = makeIteratorName.getJSType();
      // e.g. replace
      //   function(Iterator<T>): Array<T>
      // with
      //   function(Iterator<number>): Array<number>
      TemplateTypeMap typeMap =
          registry.createTemplateTypeMap(
              makeIteratorType.getTemplateTypeMap().getTemplateKeys(),
              ImmutableList.of(iterableType));
      TemplateTypeMapReplacer replacer = new TemplateTypeMapReplacer(registry, typeMap);
      makeIteratorName.setJSType(makeIteratorType.visit(replacer));
    }
    return createCall(makeIteratorName, iterator);
  }

  /**
   * Given an iterable like {@code rhs} in
   *
   * <pre>{@code
   * for await (lhs of rhs) { block(); }
   * }</pre>
   *
   * <p>returns a call node for the {@code rhs} wrapped in a {@code $jscomp.makeAsyncIterator} call.
   *
   * <pre>{@code
   * $jscomp.makeAsyncIterator(rhs)
   * }</pre>
   */
  Node createJSCompMakeAsyncIteratorCall(Node iterable, Scope scope) {
    Node makeIteratorAsyncName = createQName(scope, "$jscomp.makeAsyncIterator");
    // Since createCall (currently) doesn't handle templated functions, fill in the template types
    // of makeIteratorName manually.
    if (isAddingTypes() && !makeIteratorAsyncName.getJSType().isUnknownType()) {
      // if makeIteratorName has the unknown type, we must have not injected the required runtime
      // libraries - hopefully because this is in a test using NonInjectingCompiler.

      // e.g get `number` from `AsyncIterable<number>`
      JSType asyncIterableType =
          JsIterables.maybeBoxIterableOrAsyncIterable(iterable.getJSType(), registry)
              .orElse(unknownType);
      JSType makeAsyncIteratorType = makeIteratorAsyncName.getJSType();
      // e.g. replace
      //   function(AsyncIterable<T>): AsyncIterator<T>
      // with
      //   function(AsyncIterable<number>): AsyncIterator<number>
      TemplateTypeMap typeMap =
          registry.createTemplateTypeMap(
              makeAsyncIteratorType.getTemplateTypeMap().getTemplateKeys(),
              ImmutableList.of(asyncIterableType));
      TemplateTypeMapReplacer replacer = new TemplateTypeMapReplacer(registry, typeMap);
      makeIteratorAsyncName.setJSType(makeAsyncIteratorType.visit(replacer));
    }
    return createCall(makeIteratorAsyncName, iterable);
  }

  private JSType replaceTemplate(JSType templatedType, JSType... templateTypes) {
    TemplateTypeMap typeMap =
        registry.createTemplateTypeMap(
            templatedType.getTemplateTypeMap().getTemplateKeys(),
            ImmutableList.copyOf(templateTypes));
    TemplateTypeMapReplacer replacer = new TemplateTypeMapReplacer(registry, typeMap);
    return templatedType.visit(replacer);
  }

  /**
   * Creates a reference to $jscomp.AsyncGeneratorWrapper with the template filled in to match the
   * original function.
   *
   * @param originalFunctionType the type of the async generator function that needs transpilation
   */
  Node createAsyncGeneratorWrapperReference(JSType originalFunctionType, Scope scope) {
    Node ctor = createQName(scope, "$jscomp.AsyncGeneratorWrapper");

    if (isAddingTypes() && !ctor.getJSType().isUnknownType()) {
      // if ctor has the unknown type, we must have not injected the required runtime
      // libraries - hopefully because this is in a test using NonInjectingCompiler.

      // e.g get `number` from `AsyncIterable<number>`
      JSType yieldedType =
          originalFunctionType
              .toMaybeFunctionType()
              .getReturnType()
              .getInstantiatedTypeArgument(getNativeType(JSTypeNative.ASYNC_ITERABLE_TYPE));

      // e.g. replace
      //  AsyncGeneratorWrapper<T>
      // with
      //  AsyncGeneratorWrapper<number>
      ctor.setJSType(replaceTemplate(ctor.getJSType(), yieldedType));
    }

    return ctor;
  }

  /**
   * Creates an empty generator function with the correct return type to be an argument to
   * $jscomp.AsyncGeneratorWrapper.
   *
   * @param asyncGeneratorWrapperType the specific type of the $jscomp.AsyncGeneratorWrapper with
   *     its template filled in. Should be the type on the node returned from
   *     createAsyncGeneratorWrapperReference.
   */
  Node createEmptyAsyncGeneratorWrapperArgument(JSType asyncGeneratorWrapperType) {
    JSType generatorType = null;

    if (isAddingTypes()) {
      if (asyncGeneratorWrapperType.isUnknownType()) {
        // Not injecting libraries?
        generatorType =
            registry.createFunctionType(
                replaceTemplate(getNativeType(JSTypeNative.GENERATOR_TYPE), unknownType));
      } else {
        // Generator<$jscomp.AsyncGeneratorWrapper$ActionRecord<number>>
        JSType innerFunctionReturnType =
            Iterables.getOnlyElement(
                asyncGeneratorWrapperType.toMaybeFunctionType().getParameterTypes());
        generatorType = registry.createFunctionType(innerFunctionReturnType);
      }
    }

    return createEmptyGeneratorFunction(generatorType);
  }

  Node createJscompAsyncExecutePromiseGeneratorFunctionCall(Scope scope, Node generatorFunction) {
    String function = "asyncExecutePromiseGeneratorFunction";
    Node jscompDotAsyncExecutePromiseGeneratorFunction = createQName(scope, "$jscomp." + function);
    // TODO(bradfordcsmith): Maybe update the type to be more specific
    // Currently this method expects `function(): !Generator<?>` and returns `Promise<?>`.
    // Since we propagate type information only if type checking has already run,
    // these unknowns probably don't matter, but we should be able to be more specific with the
    // return type at least.
    return createCall(jscompDotAsyncExecutePromiseGeneratorFunction, generatorFunction);
  }

  private JSType getNativeType(JSTypeNative nativeType) {
    checkNotNull(registry, "registry is null");
    return checkNotNull(
        registry.getNativeType(nativeType), "native type not found: %s", nativeType);
  }

  /**
   * Look up the correct type for the given name in the given scope.
   *
   * <p>Returns the unknown type if no type can be found
   */
  private JSType getVarNameType(Scope scope, String name) {
    Var var = scope.getVar(name);
    JSType type = null;
    if (var != null) {
      Node nameDefinitionNode = var.getNode();
      if (nameDefinitionNode != null) {
        type = nameDefinitionNode.getJSType();
      }
    }
    if (type == null) {
      // TODO(bradfordcsmith): Consider throwing an error if the type cannot be found.
      type = unknownType;
    }
    return type;
  }

  private JSType getJsTypeForProperty(Node receiver, String propertyName) {
    // NOTE: we use both findPropertyType and getPropertyType because they are subtly
    // different: findPropertyType works on JSType, autoboxing scalars and joining unions,
    // but it returns null if the type is not found and does not handle dynamic types of
    // Function.prototype.call and .apply; whereas getPropertyType does not autobox nor
    // iterate over unions, but it does synthesize the function properties correctly, and
    // it returns unknown instead of null if the property is missing.
    JSType getpropType = null;
    JSType receiverJSType = receiver.getJSType();
    if (receiverJSType != null) {
      getpropType = receiverJSType.findPropertyType(propertyName);
      if (getpropType == null) {
        ObjectType receiverObjectType = ObjectType.cast(receiverJSType.autobox());
        getpropType =
            receiverObjectType == null
                ? unknownType
                : receiverObjectType.getPropertyType(propertyName);
      }
    }
    if (getpropType == null) {
      getpropType = unknownType;
    }
    // TODO(bradfordcsmith): Special case $jscomp.global until we annotate its type correctly.
    if (getpropType.isUnknownType()
        && propertyName.equals("global")
        && receiver.matchesQualifiedName("$jscomp")) {
      getpropType = getNativeType(JSTypeNative.GLOBAL_THIS);
    }
    return getpropType;
  }
}
