Es6RewriteClass.java

/*
 * Copyright 2014 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.Es6ToEs3Util.CANNOT_CONVERT;
import static com.google.javascript.jscomp.Es6ToEs3Util.CANNOT_CONVERT_YET;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Converts ES6 classes to valid ES5 or ES3 code.
 */
public final class Es6RewriteClass implements NodeTraversal.Callback, HotSwapCompilerPass {
  private final AbstractCompiler compiler;
  private static final FeatureSet features = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES);

  // Whether to add $jscomp.inherits(Parent, Child) for each subclass.
  private final boolean shouldAddInheritsPolyfill;

  static final DiagnosticType DYNAMIC_EXTENDS_TYPE = DiagnosticType.error(
      "JSC_DYNAMIC_EXTENDS_TYPE",
      "The class in an extends clause must be a qualified name.");

  static final DiagnosticType CLASS_REASSIGNMENT = DiagnosticType.error(
      "CLASS_REASSIGNMENT",
      "Class names defined inside a function cannot be reassigned.");

  static final DiagnosticType CONFLICTING_GETTER_SETTER_TYPE = DiagnosticType.error(
      "CONFLICTING_GETTER_SETTER_TYPE",
      "The types of the getter and setter for property ''{0}'' do not match.");

  // This function is defined in js/es6/util/inherits.js
  static final String INHERITS = "$jscomp.inherits";

  public Es6RewriteClass(AbstractCompiler compiler) {
    this(compiler, true);
  }

  public Es6RewriteClass(AbstractCompiler compiler, boolean shouldAddInheritsPolyfill) {
    this.compiler = compiler;
    this.shouldAddInheritsPolyfill = shouldAddInheritsPolyfill;
  }

  @Override
  public void process(Node externs, Node root) {
    TranspilationPasses.processTranspile(compiler, externs, features, this);
    TranspilationPasses.processTranspile(compiler, root, features, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, features, this);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case GETTER_DEF:
      case SETTER_DEF:
        if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) {
          cannotConvert(n, "ES5 getters/setters (consider using --language_out=ES5)");
          return false;
        }
        break;
      case NEW_TARGET:
        cannotConvertYet(n, "new.target");
        break;
      default:
        break;
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case CLASS:
        visitClass(t, n, parent);
        break;
      default:
        break;
    }
  }

  private void checkClassReassignment(Node clazz) {
    Node name = NodeUtil.getNameNode(clazz);
    Node enclosingFunction = NodeUtil.getEnclosingFunction(clazz);
    if (enclosingFunction == null) {
      return;
    }
    CheckClassAssignments checkAssigns = new CheckClassAssignments(name);
    NodeTraversal.traverseEs6(compiler, enclosingFunction, checkAssigns);
  }

  /**
   * Classes are processed in 3 phases:
   * <ol>
   *   <li>The class name is extracted.
   *   <li>Class members are processed and rewritten.
   *   <li>The constructor is built.
   * </ol>
   */
  private void visitClass(final NodeTraversal t, final Node classNode, final Node parent) {
    checkClassReassignment(classNode);
    // Collect Metadata
    ClassDeclarationMetadata metadata = ClassDeclarationMetadata.create(classNode, parent);

    if (metadata == null || metadata.fullClassName == null) {
      throw new IllegalStateException(
          "Can only convert classes that are declarations or the right hand"
          + " side of a simple assignment: " + classNode);
    }
    if (metadata.hasSuperClass() && !metadata.superClassNameNode.isQualifiedName()) {
      compiler.report(JSError.make(metadata.superClassNameNode, DYNAMIC_EXTENDS_TYPE));
      return;
    }

    Preconditions.checkState(NodeUtil.isStatement(metadata.insertionPoint),
        "insertion point must be a statement: %s", metadata.insertionPoint);

    Node constructor = null;
    JSDocInfo ctorJSDocInfo = null;
    // Process all members of the class
    Node classMembers = classNode.getLastChild();
    for (Node member : classMembers.children()) {
      if ((member.isComputedProp()
              && (member.getBooleanProp(Node.COMPUTED_PROP_GETTER)
                  || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)))
          || (member.isGetterDef() || member.isSetterDef())) {
        visitNonMethodMember(member, metadata);
      } else if (member.isMemberFunctionDef() && member.getString().equals("constructor")) {
        ctorJSDocInfo = member.getJSDocInfo();
        constructor = member.getFirstChild().detach();
        if (!metadata.anonymous) {
          // Turns class Foo { constructor: function() {} } into function Foo() {},
          // i.e. attaches the name to the ctor function.
          constructor.replaceChild(
              constructor.getFirstChild(), metadata.classNameNode.cloneNode());
        }
      } else if (member.isEmpty()) {
        // Do nothing.
      } else {
        Preconditions.checkState(member.isMemberFunctionDef() || member.isComputedProp(),
            "Unexpected class member:", member);
        Preconditions.checkState(!member.getBooleanProp(Node.COMPUTED_PROP_VARIABLE),
            "Member variables should have been transpiled earlier:", member);
        visitMethod(member, metadata);
      }
    }

    if (metadata.definePropertiesObjForPrototype.hasChildren()) {
      compiler.ensureLibraryInjected("util/global", false);
      Node definePropsCall =
          IR.exprResult(
              IR.call(
                  NodeUtil.newQName(compiler, "$jscomp.global.Object.defineProperties"),
                  NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
                  metadata.definePropertiesObjForPrototype));
      definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
      metadata.insertNodeAndAdvance(definePropsCall);
    }

    if (metadata.definePropertiesObjForClass.hasChildren()) {
      compiler.ensureLibraryInjected("util/global", false);
      Node definePropsCall =
          IR.exprResult(
              IR.call(
                  NodeUtil.newQName(compiler, "$jscomp.global.Object.defineProperties"),
                  NodeUtil.newQName(compiler, metadata.fullClassName),
                  metadata.definePropertiesObjForClass));
      definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
      metadata.insertNodeAndAdvance(definePropsCall);
    }

    checkNotNull(constructor);

    JSDocInfo classJSDoc = NodeUtil.getBestJSDocInfo(classNode);
    JSDocInfoBuilder newInfo = JSDocInfoBuilder.maybeCopyFrom(classJSDoc);

    newInfo.recordConstructor();

    Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode);
    if (metadata.hasSuperClass()) {
      String superClassString = metadata.superClassNameNode.getQualifiedName();
      if (newInfo.isInterfaceRecorded()) {
        newInfo.recordExtendedInterface(new JSTypeExpression(new Node(Token.BANG,
            IR.string(superClassString)),
            metadata.superClassNameNode.getSourceFileName()));
      } else {
        if (shouldAddInheritsPolyfill && !classNode.isFromExterns()) {
          Node classNameNode = NodeUtil.newQName(compiler, metadata.fullClassName)
              .useSourceInfoIfMissingFrom(metadata.classNameNode);
          Node superClassNameNode = metadata.superClassNameNode.cloneTree();

          Node inherits = IR.call(
              NodeUtil.newQName(compiler, INHERITS), classNameNode, superClassNameNode);
          Node inheritsCall = IR.exprResult(inherits);
          compiler.ensureLibraryInjected("es6/util/inherits", false);

          inheritsCall.useSourceInfoIfMissingFromForTree(metadata.superClassNameNode);
          enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement);
        }
        newInfo.recordBaseType(new JSTypeExpression(new Node(Token.BANG,
            IR.string(superClassString)),
            metadata.superClassNameNode.getSourceFileName()));
      }
    }

    addTypeDeclarations(metadata, enclosingStatement);

    updateClassJsDoc(ctorJSDocInfo, newInfo);

    if (NodeUtil.isStatement(classNode)) {
      constructor.getFirstChild().setString("");
      Node ctorVar = IR.let(metadata.classNameNode.cloneNode(), constructor);
      ctorVar.useSourceInfoIfMissingFromForTree(classNode);
      parent.replaceChild(classNode, ctorVar);
    } else {
      parent.replaceChild(classNode, constructor);
    }
    NodeUtil.markFunctionsDeleted(classNode, compiler);

    if (NodeUtil.isStatement(constructor)) {
      constructor.setJSDocInfo(newInfo.build());
    } else if (parent.isName()) {
      // The constructor function is the RHS of a var statement.
      // Add the JSDoc to the VAR node.
      Node var = parent.getParent();
      var.setJSDocInfo(newInfo.build());
    } else if (constructor.getParent().isName()) {
      // Is a newly created VAR node.
      Node var = constructor.getGrandparent();
      var.setJSDocInfo(newInfo.build());
    } else if (parent.isAssign()) {
      // The constructor function is the RHS of an assignment.
      // Add the JSDoc to the ASSIGN node.
      parent.setJSDocInfo(newInfo.build());
    } else {
      throw new IllegalStateException("Unexpected parent node " + parent);
    }

    constructor.putBooleanProp(Node.IS_ES6_CLASS, true);
    t.reportCodeChange();
  }

  /**
   * @param ctorInfo the JSDocInfo from the constructor method of the ES6 class.
   * @param newInfo the JSDocInfo that will be added to the constructor function in the ES3 output
   */
  private void updateClassJsDoc(@Nullable JSDocInfo ctorInfo, JSDocInfoBuilder newInfo) {
    // Classes are @struct by default.
    if (!newInfo.isUnrestrictedRecorded() && !newInfo.isDictRecorded()
        && !newInfo.isStructRecorded()) {
      newInfo.recordStruct();
    }

    if (ctorInfo != null) {
      if (!ctorInfo.getSuppressions().isEmpty()) {
        newInfo.recordSuppressions(ctorInfo.getSuppressions());
      }

      for (String param : ctorInfo.getParameterNames()) {
        newInfo.recordParameter(param, ctorInfo.getParameterType(param));
        newInfo.recordParameterDescription(param, ctorInfo.getDescriptionForParameter(param));
      }

      for (JSTypeExpression thrown : ctorInfo.getThrownTypes()) {
        newInfo.recordThrowType(thrown);
        newInfo.recordThrowDescription(thrown, ctorInfo.getThrowsDescriptionForType(thrown));
      }

      JSDocInfo.Visibility visibility = ctorInfo.getVisibility();
      if (visibility != null && visibility != JSDocInfo.Visibility.INHERITED) {
        newInfo.recordVisibility(visibility);
      }

      if (ctorInfo.isDeprecated()) {
        newInfo.recordDeprecated();
      }

      if (ctorInfo.getDeprecationReason() != null
          && !newInfo.isDeprecationReasonRecorded()) {
        newInfo.recordDeprecationReason(ctorInfo.getDeprecationReason());
      }

      newInfo.mergePropertyBitfieldFrom(ctorInfo);

      for (String templateType : ctorInfo.getTemplateTypeNames()) {
        newInfo.recordTemplateTypeName(templateType);
      }
    }
  }

  /**
   * @param node A getter or setter node.
   */
  @Nullable
  private JSTypeExpression getTypeFromGetterOrSetter(Node node) {
    JSDocInfo info = node.getJSDocInfo();
    if (info != null) {
      boolean getter = node.isGetterDef() || node.getBooleanProp(Node.COMPUTED_PROP_GETTER);
      if (getter && info.getReturnType() != null) {
        return info.getReturnType();
      } else {
        Set<String> paramNames = info.getParameterNames();
        if (paramNames.size() == 1) {
          JSTypeExpression paramType =
              info.getParameterType(Iterables.getOnlyElement(info.getParameterNames()));
          if (paramType != null) {
            return paramType;
          }
        }
      }
    }

    return null;
  }

  /**
   * @param member A getter or setter, or a computed property that is a getter/setter.
   */
  private void addToDefinePropertiesObject(ClassDeclarationMetadata metadata, Node member) {
    Node obj =
        member.isStaticMember()
            ? metadata.definePropertiesObjForClass
            : metadata.definePropertiesObjForPrototype;
    Node prop =
        member.isComputedProp()
            ? NodeUtil.getFirstComputedPropMatchingKey(obj, member.getFirstChild())
            : NodeUtil.getFirstPropMatchingKey(obj, member.getString());
    if (prop == null) {
      prop =
          IR.objectlit(
              IR.stringKey("configurable", IR.trueNode()),
              IR.stringKey("enumerable", IR.trueNode()));
      if (member.isComputedProp()) {
        obj.addChildToBack(IR.computedProp(member.getFirstChild().cloneTree(), prop));
      } else {
        obj.addChildToBack(IR.stringKey(member.getString(), prop));
      }
    }

    Node function = member.getLastChild();
    JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(
        NodeUtil.getBestJSDocInfo(function));

    info.recordThisType(new JSTypeExpression(new Node(
        Token.BANG, IR.string(metadata.fullClassName)), member.getSourceFileName()));
    Node stringKey =
        IR.stringKey(
            (member.isGetterDef() || member.getBooleanProp(Node.COMPUTED_PROP_GETTER))
                ? "get"
                : "set",
            function.detach());
    stringKey.setJSDocInfo(info.build());
    prop.addChildToBack(stringKey);
    prop.useSourceInfoIfMissingFromForTree(member);
  }

  /**
   * Visits class members other than simple methods: Getters, setters, and computed properties.
   */
  private void visitNonMethodMember(Node member, ClassDeclarationMetadata metadata) {
    if (member.isComputedProp() && member.isStaticMember()) {
      cannotConvertYet(member, "Static computed property");
      return;
    }
    if (member.isComputedProp() && !member.getFirstChild().isQualifiedName()) {
      cannotConvert(member.getFirstChild(), "Computed property with non-qualified-name key");
      return;
    }

    JSTypeExpression typeExpr = getTypeFromGetterOrSetter(member);
    addToDefinePropertiesObject(metadata, member);

    Map<String, JSDocInfo> membersToDeclare;
    String memberName;
    if (member.isComputedProp()) {
      checkState(!member.isStaticMember());
      membersToDeclare = metadata.prototypeComputedPropsToDeclare;
      memberName = member.getFirstChild().getQualifiedName();
    } else {
      membersToDeclare =
          member.isStaticMember()
              ? metadata.classMembersToDeclare
              : metadata.prototypeMembersToDeclare;
      memberName = member.getString();
    }
    JSDocInfo existingJSDoc = membersToDeclare.get(memberName);
    JSTypeExpression existingType = existingJSDoc == null ? null : existingJSDoc.getType();
    if (existingType != null && typeExpr != null && !existingType.equals(typeExpr)) {
      compiler.report(JSError.make(member, CONFLICTING_GETTER_SETTER_TYPE, memberName));
    } else {
      JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false);
      if (member.getJSDocInfo() != null && member.getJSDocInfo().isExport()) {
        jsDoc.recordExport();
        jsDoc.recordVisibility(Visibility.PUBLIC);
      }
      if (member.getJSDocInfo() != null && member.getJSDocInfo().isOverride()) {
        jsDoc.recordOverride();
      } else if (typeExpr == null) {
        typeExpr = new JSTypeExpression(new Node(Token.QMARK), member.getSourceFileName());
      }
      if (typeExpr != null) {
        jsDoc.recordType(typeExpr.copy());
      }
      if (member.isStaticMember() && !member.isComputedProp()) {
        jsDoc.recordNoCollapse();
      }
      membersToDeclare.put(memberName, jsDoc.build());
    }
  }

  /**
   * Handles transpilation of a standard class member function. Getters, setters, and the
   * constructor are not handled here.
   */
  private void visitMethod(Node member, ClassDeclarationMetadata metadata) {
    Node qualifiedMemberAccess = getQualifiedMemberAccess(
        member,
        NodeUtil.newQName(compiler, metadata.fullClassName),
        NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"));
    Node method = member.getLastChild().detach();

    Node assign = IR.assign(qualifiedMemberAccess, method);
    assign.useSourceInfoIfMissingFromForTree(member);

    JSDocInfo info = member.getJSDocInfo();
    if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) {
      JSDocInfoBuilder memberDoc = JSDocInfoBuilder.maybeCopyFrom(info);
      memberDoc.recordThisType(
          new JSTypeExpression(new Node(Token.BANG, new Node(Token.QMARK)),
          member.getSourceFileName()));
      info = memberDoc.build();
    }
    if (info != null) {
      assign.setJSDocInfo(info);
    }

    Node newNode = NodeUtil.newExpr(assign);
    metadata.insertNodeAndAdvance(newNode);
  }

  /**
   * Add declarations for properties that were defined with a getter and/or setter,
   * so that the typechecker knows those properties exist on the class.
   * This is a temporary solution. Eventually, the type checker should understand
   * Object.defineProperties calls directly.
   */
  private void addTypeDeclarations(ClassDeclarationMetadata metadata, Node insertionPoint) {
    for (Map.Entry<String, JSDocInfo> entry : metadata.prototypeMembersToDeclare.entrySet()) {
      String declaredMember = entry.getKey();
      Node declaration = IR.getprop(
          NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
          IR.string(declaredMember));
      declaration.setJSDocInfo(entry.getValue());
      declaration =
          IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
      insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
      insertionPoint = declaration;
    }
    for (Map.Entry<String, JSDocInfo> entry : metadata.classMembersToDeclare.entrySet()) {
      String declaredMember = entry.getKey();
      Node declaration = IR.getprop(
          NodeUtil.newQName(compiler, metadata.fullClassName),
          IR.string(declaredMember));
      declaration.setJSDocInfo(entry.getValue());
      declaration =
          IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
      insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
      insertionPoint = declaration;
    }
    for (Map.Entry<String, JSDocInfo> entry : metadata.prototypeComputedPropsToDeclare.entrySet()) {
      String declaredMember = entry.getKey();
      Node declaration = IR.getelem(
          NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
          NodeUtil.newQName(compiler, declaredMember));
      declaration.setJSDocInfo(entry.getValue());
      declaration =
          IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
      insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
      insertionPoint = declaration;
    }
  }

  /**
   * Constructs a Node that represents an access to the given class member, qualified by either the
   * static or the instance access context, depending on whether the member is static.
   *
   * <p><b>WARNING:</b> {@code member} may be modified/destroyed by this method, do not use it
   * afterwards.
   */
  private static Node getQualifiedMemberAccess(Node member,
      Node staticAccess, Node instanceAccess) {
    Node context = member.isStaticMember() ? staticAccess : instanceAccess;
    context = context.cloneTree();
    context.makeNonIndexableRecursive();
    if (member.isComputedProp()) {
      return IR.getelem(context, member.removeFirstChild());
    } else {
      Node methodName = member.getFirstFirstChild();
      return IR.getprop(context, IR.string(member.getString()))
          .useSourceInfoFromForTree(methodName);
    }
  }

  private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback {
    private final Node className;

    public CheckClassAssignments(Node className) {
      this.className = className;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!n.isAssign() || n.getFirstChild() == className) {
        return;
      }
      if (className.matchesQualifiedName(n.getFirstChild())) {
        compiler.report(JSError.make(n, CLASS_REASSIGNMENT));
      }
    }

  }

  private void cannotConvert(Node n, String message) {
    compiler.report(JSError.make(n, CANNOT_CONVERT, message));
  }

  /**
   * Warns the user that the given ES6 feature cannot be converted to ES3
   * because the transpilation is not yet implemented. A call to this method
   * is essentially a "TODO(tbreisacher): Implement {@code feature}" comment.
   */
  private void cannotConvertYet(Node n, String feature) {
    compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature));
  }

  /**
   * Represents static metadata on a class declaration expression - i.e. the qualified name that a
   * class declares (directly or by assignment), whether it's anonymous, and where transpiled code
   * should be inserted (i.e. which object will hold the prototype after transpilation).
   */
  static class ClassDeclarationMetadata {
    /** A statement node. Transpiled methods etc of the class are inserted after this node. */
    private Node insertionPoint;

    /**
     * An object literal node that will be used in a call to Object.defineProperties, to add getters
     * and setters to the prototype.
     */
    private final Node definePropertiesObjForPrototype;

    /**
     * An object literal node that will be used in a call to Object.defineProperties, to add getters
     * and setters to the class.
     */
    private final Node definePropertiesObjForClass;

    // Normal declarations to be added to the prototype: Foo.prototype.bar
    private final Map<String, JSDocInfo> prototypeMembersToDeclare;

    // Computed property declarations to be added to the prototype: Foo.prototype[bar]
    private final Map<String, JSDocInfo> prototypeComputedPropsToDeclare;

    // Normal declarations to be added to the class: Foo.bar
    private final Map<String, JSDocInfo> classMembersToDeclare;

    /**
     * The fully qualified name of the class, which will be used in the output. May come from the
     * class itself or the LHS of an assignment.
     */
    final String fullClassName;
    /** Whether the constructor function in the output should be anonymous. */
    final boolean anonymous;
    final Node classNameNode;
    final Node superClassNameNode;

    private ClassDeclarationMetadata(Node insertionPoint, String fullClassName,
        boolean anonymous, Node classNameNode, Node superClassNameNode) {
      this.insertionPoint = insertionPoint;
      this.definePropertiesObjForClass = IR.objectlit();
      this.definePropertiesObjForPrototype = IR.objectlit();
      this.prototypeMembersToDeclare = new LinkedHashMap<>();
      this.prototypeComputedPropsToDeclare = new LinkedHashMap<>();
      this.classMembersToDeclare = new LinkedHashMap<>();
      this.fullClassName = fullClassName;
      this.anonymous = anonymous;
      this.classNameNode = classNameNode;
      this.superClassNameNode = superClassNameNode;
    }

    static ClassDeclarationMetadata create(Node classNode, Node parent) {
      Node classNameNode = classNode.getFirstChild();
      Node superClassNameNode = classNameNode.getNext();

      // If this is a class statement, or a class expression in a simple
      // assignment or var statement, convert it. In any other case, the
      // code is too dynamic, so return null.
      if (NodeUtil.isClassDeclaration(classNode)) {
        return new ClassDeclarationMetadata(classNode, classNameNode.getString(), false,
            classNameNode, superClassNameNode);
      } else if (parent.isAssign() && parent.getParent().isExprResult()) {
        // Add members after the EXPR_RESULT node:
        // example.C = class {}; example.C.prototype.foo = function() {};
        String fullClassName = parent.getFirstChild().getQualifiedName();
        if (fullClassName == null) {
          return null;
        }
        return new ClassDeclarationMetadata(parent.getParent(), fullClassName, true, classNameNode,
            superClassNameNode);
      } else if (parent.isExport()) {
        return new ClassDeclarationMetadata(
            classNode, classNameNode.getString(), false, classNameNode, superClassNameNode);
      } else if (parent.isName()) {
        // Add members after the 'var' statement.
        // var C = class {}; C.prototype.foo = function() {};
        return new ClassDeclarationMetadata(parent.getParent(), parent.getString(), true,
            classNameNode, superClassNameNode);
      } else {
        // Cannot handle this class declaration.
        return null;
      }
    }

    void insertNodeAndAdvance(Node newNode) {
      insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
      insertionPoint = newNode;
    }

    boolean hasSuperClass() {
      return !superClassNameNode.isEmpty();
    }
  }
}