All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.Es6ConvertSuperConstructorCalls Maven / Gradle / Ivy

/*
 * 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.checkArgument;
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 com.google.common.collect.Iterables;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

/** Converts {@code super()} calls. */
public final class Es6ConvertSuperConstructorCalls implements NodeTraversal.Callback {
  private static final String TMP_ERROR = "$jscomp$tmp$error";
  private static final String SUPER_THIS = "$jscomp$super$this";

  /** Stores superCalls for a constructor. */
  private static final class ConstructorData {
    final Node constructor;
    final List superCalls;

    ConstructorData(Node constructor) {
      this.constructor = constructor;
      superCalls = new ArrayList<>();
    }
  }

  private final AbstractCompiler compiler;
  private final Deque constructorDataStack;
  private final AstFactory astFactory;
  private GlobalNamespace globalNamespace;

  public Es6ConvertSuperConstructorCalls(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.astFactory = compiler.createAstFactory();
    this.constructorDataStack = new ArrayDeque<>();
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    if (n.isFunction()) {
      // TODO(bradfordcsmith): Avoid creating data for non-constructor functions.
      constructorDataStack.push(new ConstructorData(n));
    } else if (n.isSuper()) {
      // super(args) or super.prop
      checkState(n.isFirstChildOf(parent), parent);
      if (parent.isGetProp()) {
        // TODO(bradfordcsmith): `super.prop` should have been removed before this code executes,
        //     but instead is being left untranspiled when there's no `extends` clause, so
        //     we have to report that problem here.
        t.report(n, Es6ToEs3Util.CANNOT_CONVERT_YET, "super access with no extends clause");
        return false;
      }
      // must be super(args)
      checkState(parent.isCall(), parent);
      ConstructorData constructorData = checkNotNull(constructorDataStack.peek());
      constructorData.superCalls.add(parent);
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    ConstructorData constructorData = constructorDataStack.peek();
    if (constructorData != null && n == constructorData.constructor) {
      constructorDataStack.pop();
      visitSuper(t, constructorData);
    }
  }

  private void visitSuper(NodeTraversal t, ConstructorData constructorData) {
    // NOTE: When this pass runs:
    // -   ES6 classes have already been rewritten as ES5 functions.
    // -   All subclasses have $jscomp.inherits() calls connecting them to their parent class.
    // -   All instances of `super` that are not super constructor calls have been rewritten.
    Node constructor = constructorData.constructor;
    List superCalls = constructorData.superCalls;
    if (superCalls.isEmpty()) {
      return; // nothing to do
    }
    if (constructor.isFromExterns()) {
      // This class is defined in an externs file, so it's only a stub, not the actual
      // implementation that should be instantiated.
      // A call to super() shouldn't actually exist for a stub and is problematic to transpile,
      // so just drop it.
      for (Node superCall : superCalls) {
        Node enclosingStatement = NodeUtil.getEnclosingStatement(superCall);
        Node enclosingScope = enclosingStatement.getParent();
        enclosingStatement.detach();
        compiler.reportChangeToEnclosingScope(enclosingScope);
      }
    } else {
      // Find the `foo.SuperClass` part of `$jscomp.inherits(foo.SubClass, foo.SuperClass)`
      Node superClassNameNode = getSuperClassQNameNode(constructor);

      String superClassQName = superClassNameNode.getQualifiedName();
      JSType thisType = getTypeOfThisForConstructor(constructorData.constructor);
      if (isNativeObjectClass(t, superClassQName)) {
        // There's no need to call Object as a super constructor, so just replace the call with
        // `this`, which is its correct return value.
        // TODO(bradfordcsmith): Although unlikely, super() could have argument expressions with
        //     side-effects.
        for (Node superCall : superCalls) {
          Node thisNode = astFactory.createThis(thisType).useSourceInfoFrom(superCall);
          superCall.replaceWith(thisNode);
          compiler.reportChangeToEnclosingScope(thisNode);
        }
      } else if (isUnextendableNativeClass(t, superClassQName)) {
        compiler.report(
            JSError.make(
                constructor, CANNOT_CONVERT, "extending native class: " + superClassQName));
      } else if (isNativeErrorClass(t, superClassQName)) {
        for (Node superCall : superCalls) {
          Node newSuperCall = createNewSuperCall(superClassNameNode, superCall, thisType);
          replaceNativeErrorSuperCall(superCall, newSuperCall);
        }
      } else if (isKnownToReturnOnlyUndefined(superClassQName)) {
        // super() will not change the value of `this`.
        for (Node superCall : superCalls) {
          Node newSuperCall = createNewSuperCall(superClassNameNode, superCall, thisType);
          Node superCallParent = superCall.getParent();
          if (superCallParent.hasOneChild() && NodeUtil.isStatement(superCallParent)) {
            // super() is a statement unto itself
            superCallParent.replaceChild(superCall, newSuperCall);
          } else {
            // super() is part of an expression, so it must return `this`.
            superCallParent.replaceChild(
                superCall,
                astFactory
                    .createComma(newSuperCall, astFactory.createThis(thisType))
                    .useSourceInfoIfMissingFromForTree(superCall));
          }
          compiler.reportChangeToEnclosingScope(superCallParent);
        }
      } else {
        Node constructorBody = checkNotNull(constructor.getChildAtIndex(2));
        Node firstStatement = constructorBody.getFirstChild();
        Node firstSuperCall = superCalls.get(0);

        if (constructorBody.hasOneChild()
            && firstStatement.isExprResult()
            && firstStatement.hasOneChild()
            && firstStatement.getFirstChild() == firstSuperCall) {
          checkState(superCalls.size() == 1, constructor);
          // Super call is the entire constructor, so just replace it with.
          // `return  || this;`
          Node newReturn =
              astFactory.createOr(
                  createNewSuperCall(
                      superClassNameNode, superCalls.get(0), superCalls.get(0).getJSType()),
                  astFactory.createThis(thisType));
          constructorBody.replaceChild(
              firstStatement,
              IR.returnNode(newReturn).useSourceInfoIfMissingFromForTree(firstStatement));
        } else {
          final JSType typeOfThis = getTypeOfThisForConstructor(constructor);
          // `this` -> `$jscomp$super$this` throughout the constructor body,
          // except for super() calls.
          updateThisToSuperThis(typeOfThis, constructorBody, superCalls);
          // Start constructor with `var $jscomp$super$this;`
          constructorBody.addChildToFront(
              IR.var(astFactory.createName(SUPER_THIS, typeOfThis))
                  .useSourceInfoFromForTree(constructorBody));
          // End constructor with `return $jscomp$super$this;`
          constructorBody.addChildToBack(
              IR.returnNode(astFactory.createName(SUPER_THIS, typeOfThis))
                  .useSourceInfoFromForTree(constructorBody));
          // Replace each super() call with `($jscomp$super$this =  || this)`
          for (Node superCall : superCalls) {
            Node newSuperCall = createNewSuperCall(superClassNameNode, superCall, typeOfThis);
            superCall.replaceWith(
                astFactory
                    .createAssign(
                        astFactory.createName(SUPER_THIS, typeOfThis),
                        astFactory.createOr(newSuperCall, astFactory.createThis(typeOfThis)))
                    .useSourceInfoIfMissingFromForTree(superCall));
          }
        }
        compiler.reportChangeToEnclosingScope(constructorBody);
      }
    }
  }

  private boolean isKnownToReturnOnlyUndefined(String functionQName) {
    if (globalNamespace == null) {
      return false;
    }
    Name globalName = globalNamespace.getSlot(functionQName);
    if (globalName == null) {
      return false;
    }

    Ref declarationRef = globalName.getDeclaration();
    if (declarationRef == null) {
      for (Ref ref : globalName.getRefs()) {
        if (ref.isSet()) {
          declarationRef = ref;
        }
      }
    }
    if (declarationRef == null) {
      return false;
    }

    Node declaredVarOrProp = declarationRef.getNode();
    if (declaredVarOrProp.isFromExterns()) {
      return false;
    }

    Node declaration = declaredVarOrProp.getParent();
    Node declaredValue = null;
    if (declaration.isFunction()) {
      declaredValue = declaration;
    } else if (NodeUtil.isNameDeclaration(declaration) && declaredVarOrProp.isName()) {
      if (declaredVarOrProp.hasChildren()) {
        declaredValue = checkNotNull(declaredVarOrProp.getFirstChild());
      } else {
        return false; // Declaration without an assigned value.
      }
    } else if (declaration.isAssign() && declaration.getFirstChild() == declaredVarOrProp) {
      declaredValue = checkNotNull(declaration.getSecondChild());
    } else if (declaration.isObjectLit() && declaredVarOrProp.hasOneChild()){
      declaredValue = checkNotNull(declaredVarOrProp.getFirstChild());
    } else {
      throw new IllegalStateException(
          "Unexpected declaration format:\n" + declaration.toStringTree());
    }

    if (declaredValue.isFunction()) {
      Node functionBody = checkNotNull(declaredValue.getChildAtIndex(2));
      return !(new UndefinedReturnValueCheck().mayReturnDefinedValue(functionBody));
    } else if (declaredValue.isQualifiedName()) {
      return isKnownToReturnOnlyUndefined(declaredValue.getQualifiedName());
    } else {
      // TODO(bradfordcsmith): What cases are these? Can we do better?
      return false;
    }
  }

  private class UndefinedReturnValueCheck {
    private boolean foundNonEmptyReturn;

    boolean mayReturnDefinedValue(Node functionBody) {
      foundNonEmptyReturn = false;
      NodeTraversal.Callback checkForDefinedReturnValue =
          new NodeTraversal.AbstractShallowCallback() {

            @Override
            public void visit(NodeTraversal t, Node n, Node parent) {
              if (!foundNonEmptyReturn) {
                if (n.isReturn()
                    && n.hasChildren()
                    && !n.getFirstChild().matchesName("undefined")) {
                  foundNonEmptyReturn = true;
                }
              }
            }
          };
      NodeTraversal.traverse(compiler, functionBody, checkForDefinedReturnValue);
      return foundNonEmptyReturn;
    }
  }

  /**
   * Returns a transpiled version of the super constructor call.
   *
   * 

The children of the passed in `superCall` are all removed from it by this method, but the * existing call itself is not replaced in the AST yet. The returned node is not yet attached to * the AST. */ private Node createNewSuperCall(Node superClassQNameNode, Node superCall, JSType thisType) { checkArgument(superClassQNameNode.isQualifiedName(), superClassQNameNode); checkArgument(superCall.isCall(), superCall); Node callee = superCall.removeFirstChild(); checkState(callee.isSuper(), callee); List args = new ArrayList<>(); boolean hasSpreadArg = false; while (superCall.hasChildren()) { final Node arg = superCall.removeFirstChild(); hasSpreadArg = hasSpreadArg || arg.isSpread(); args.add(arg); } // Node to which args should be appended if (hasSpreadArg) { // We want to convert // // super(x, ...params, y) // to // Foo.apply(this, [x, ...params, y]) // // because, after transpilation of spread this becomes // // Foo.apply(this, [x, $jscomp.arrayFromIterable(params), y]) // // If we used `call`, we'd get this nonsense instead // // Foo.call.apply(Foo, [this, x, $jscomp.arrayFromIterable(params), y]) Node superClassDotApply = astFactory .createGetProp(superClassQNameNode.cloneTree(), "apply") .useSourceInfoFromForTree(callee); // Create `SuperClass.call(this)` Node newSuperCall = astFactory.createCall(superClassDotApply).useSourceInfoFrom(superCall); newSuperCall.addChildToBack(astFactory.createThis(thisType).useSourceInfoFrom(callee)); newSuperCall.putBooleanProp(Node.FREE_CALL, false); // callee is now a getprop // It's very common to just have `super(...arguments)`, because we generate constructors // containing that for extending classes that don't have an explicit constructor. // For that case it's more efficient to just convert `super(...arguments)` to // `SuperClass.apply(this, arguments)` here rather than relying on later optimizations to // convert `[...arguments]` to `arguments`. if (isSingleSpreadOfArguments(args)) { newSuperCall.addChildToBack(Iterables.getOnlyElement(args).getOnlyChild().detach()); } else { newSuperCall.addChildToBack(astFactory.createArraylit(args).useSourceInfoFrom(superCall)); } return newSuperCall; } else { // We want to convert // // super(arg1, arg2) // to // Foo.call(this, arg1, arg2) // // Using `call` is shorter than using `apply`. Node superClassDotCall = astFactory .createGetProp(superClassQNameNode.cloneTree(), "call") .useSourceInfoFromForTree(callee); Node newSuperCall = astFactory.createCall(superClassDotCall).useSourceInfoFrom(superCall); newSuperCall.addChildToBack(astFactory.createThis(thisType).useSourceInfoFrom(callee)); newSuperCall.putBooleanProp(Node.FREE_CALL, false); // callee is now a getprop for (Node arg : args) { newSuperCall.addChildToBack(arg); } return newSuperCall; } } private static boolean isSingleSpreadOfArguments(List nodeList) { return nodeList.size() == 1 && isSpreadOfArguments(Iterables.getOnlyElement(nodeList)); } private static boolean isSpreadOfArguments(Node node) { return node.isSpread() && node.getOnlyChild().matchesName("arguments"); } private void replaceNativeErrorSuperCall(Node superCall, Node newSuperCall) { // The native error class constructors always return a new object instead of initializing // `this`, so a workaround is needed. Node superStatement = NodeUtil.getEnclosingStatement(superCall); Node body = superStatement.getParent(); checkState(body.isBlock(), body); JSType thisType = newSuperCall.getJSType(); // var $jscomp$tmp$error; Node getError = IR.var(astFactory.createName(TMP_ERROR, thisType)) .useSourceInfoIfMissingFromForTree(superCall); body.addChildBefore(getError, superStatement); // Create an expression to initialize `this` from temporary Error object at the point // where super.apply() was called. // $jscomp$tmp$error = Error.call(this, ...), Node getTmpError = astFactory.createAssign(astFactory.createName(TMP_ERROR, thisType), newSuperCall); // this.message = $jscomp$tmp$error.message, Node copyMessage = astFactory.createAssign( astFactory.createGetProp(astFactory.createThis(thisType), "message"), astFactory.createGetProp(astFactory.createName(TMP_ERROR, thisType), "message")); // Old versions of IE Don't set stack until the object is thrown, and won't set it then // if it already exists on the object. // ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack) Node setStack = astFactory.createAnd( astFactory.createIn( astFactory.createString("stack"), astFactory.createName(TMP_ERROR, thisType)), astFactory.createAssign( astFactory.createGetProp(astFactory.createThis(thisType), "stack"), astFactory.createGetProp(astFactory.createName(TMP_ERROR, thisType), "stack"))); Node superErrorExpr = astFactory .createCommas(getTmpError, copyMessage, setStack, astFactory.createThis(thisType)) .useSourceInfoIfMissingFromForTree(superCall); superCall.replaceWith(superErrorExpr); compiler.reportChangeToEnclosingScope(superErrorExpr); } private boolean isNativeObjectClass(NodeTraversal t, String className) { return className.equals("Object") && !isDefinedInSources(t, className); } private boolean isNativeErrorClass(NodeTraversal t, String superClassName) { switch (superClassName) { // All Error classes listed in the ECMAScript spec as of 2016 case "Error": case "EvalError": case "RangeError": case "ReferenceError": case "SyntaxError": case "TypeError": case "URIError": return !isDefinedInSources(t, superClassName); default: return false; } } /** * Is the given class a native class for which we cannot properly transpile extension? * @param t * @param className */ private boolean isUnextendableNativeClass(NodeTraversal t, String className) { // This list originally taken from the list of built-in objects at // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference // as of 2016-10-22. // - Intl.* classes were left out, because it doesn't seem worth the extra effort // of handling the qualified name. // - Deprecated and experimental classes were left out. switch (className) { case "Array": case "ArrayBuffer": case "Boolean": case "DataView": case "Date": case "Float32Array": case "Function": case "Generator": case "GeneratorFunction": case "Int16Array": case "Int32Array": case "Int8Array": case "InternalError": case "Map": case "Number": case "Promise": case "Proxy": case "RegExp": case "Set": case "String": case "Symbol": case "TypedArray": case "Uint16Array": case "Uint32Array": case "Uint8Array": case "Uint8ClampedArray": case "WeakMap": case "WeakSet": return !isDefinedInSources(t, className); default: return false; } } /** * Is a variable with the given name defined in the source code being compiled? * *

Please note that the call to {@code t.getScope()} is expensive, so we should avoid * calling this method when possible. * @param t * @param varName */ private boolean isDefinedInSources(NodeTraversal t, String varName) { Var objectVar = t.getScope().getVar(varName); return objectVar != null && !objectVar.isExtern(); } private void updateThisToSuperThis( final JSType typeOfThis, Node constructorBody, final List superCalls) { NodeTraversal.Callback replaceThisWithSuperThis = new NodeTraversal.Callback() { @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (superCalls.contains(n)) { return false; // Leave `this` intact on super calls. } else if (n.isFunction() && !n.isArrowFunction()) { // Don't replace `this` in non-arrow function definitions. return false; } else { return true; } } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isThis()) { Node superThis = astFactory.createName(SUPER_THIS, n.getJSType()).useSourceInfoFrom(n); parent.replaceChild(n, superThis); } else if (n.isReturn() && !n.hasChildren()) { // An empty return needs to be changed to return $jscomp$super$this n.addChildToFront(astFactory.createName(SUPER_THIS, typeOfThis).useSourceInfoFrom(n)); } } }; NodeTraversal.traverse(compiler, constructorBody, replaceThisWithSuperThis); } private JSType getTypeOfThisForConstructor(Node constructor) { checkArgument(constructor.isFunction(), constructor); // If typechecking has run, all function nodes should have a JSType. Nodes that were in a CAST // will also have the TYPE_BEFORE_CAST property, which is null for other nodes. final JSType constructorTypeBeforeCast = constructor.getJSTypeBeforeCast(); final JSType constructorType = constructorTypeBeforeCast != null ? constructorTypeBeforeCast : constructor.getJSType(); if (constructorType == null) { return null; // Type checking passes must not have run. } checkState(constructorType.isFunctionType()); return constructorType.toMaybeFunctionType().getTypeOfThis(); } private Node getSuperClassQNameNode(Node constructor) { String className = NodeUtil.getNameNode(constructor).getQualifiedName(); Node constructorStatement = checkNotNull(NodeUtil.getEnclosingStatement(constructor)); Node superClassNameNode = null; for (Node statement = constructorStatement.getNext(); statement != null; statement = statement.getNext()) { superClassNameNode = getSuperClassNameNodeIfIsInheritsStatement(statement, className); if (superClassNameNode != null) { break; } } return checkNotNull(superClassNameNode, "$jscomp.inherits() call not found."); } private Node getSuperClassNameNodeIfIsInheritsStatement(Node statement, String className) { // $jscomp.inherits(ChildClass, SuperClass); if (!statement.isExprResult()) { return null; } Node callNode = statement.getFirstChild(); if (!callNode.isCall()) { return null; } Node jscompDotInherits = callNode.getFirstChild(); if (!jscompDotInherits.matchesQualifiedName("$jscomp.inherits")) { return null; } Node classNameNode = checkNotNull(jscompDotInherits.getNext()); if (classNameNode.matchesQualifiedName(className)) { return checkNotNull(classNameNode.getNext()); } else { return null; } } void setGlobalNamespace(GlobalNamespace globalNamespace) { this.globalNamespace = globalNamespace; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy