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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * 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 - 2024 Weber Informatics LLC | Privacy Policy