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.Es6ToEs3Converter.CANNOT_CONVERT;

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.Token;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

/** Converts {@code super()} calls. This has to run after typechecking. */
public final class Es6ConvertSuperConstructorCalls
implements NodeTraversal.Callback, HotSwapCompilerPass {
  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 GlobalNamespace globalNamespace;

  public Es6ConvertSuperConstructorCalls(AbstractCompiler compiler) {
    this.compiler = compiler;
    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()) {
      Node superCall = parent.isCall() ? parent : parent.getParent();
      checkState(superCall.isCall(), superCall);
      ConstructorData constructorData = checkNotNull(constructorDataStack.peek());
      constructorData.superCalls.add(superCall);
    }
    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 instances of super() that are not super constructor calls have been rewritten.
    // -   However, if the original call used spread (e.g. super(...list)), then spread
    //     transpilation will have turned that into something like
    //     super.apply(null, $jscomp$expanded$args).
    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 {
      String superClassQName = getSuperClassQName(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 = IR.thisNode().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(superClassQName, superCall);
          replaceNativeErrorSuperCall(superCall, newSuperCall);
        }
      } else if (isKnownToReturnOnlyUndefined(superClassQName)) {
        // super() will not change the value of `this`.
        for (Node superCall : superCalls) {
          Node newSuperCall = createNewSuperCall(superClassQName, superCall);
          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,
                IR.comma(newSuperCall, IR.thisNode().useSourceInfoFrom(superCall))
                    .useSourceInfoFrom(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;`
          constructorBody.replaceChild(
              firstStatement,
              IR.returnNode(
                      IR.or(createNewSuperCall(superClassQName, superCalls.get(0)), IR.thisNode()))
                  .useSourceInfoIfMissingFromForTree(firstStatement));
        } else {
          // `this` -> `$jscomp$super$this` throughout the constructor body,
          // except for super() calls.
          updateThisToSuperThis(constructorBody, superCalls);
          // Start constructor with `var $jscomp$super$this;`
          constructorBody.addChildToFront(
              IR.var(IR.name(SUPER_THIS)).useSourceInfoFromForTree(constructorBody));
          // End constructor with `return $jscomp$super$this;`
          constructorBody.addChildToBack(
              IR.returnNode(IR.name(SUPER_THIS)).useSourceInfoFromForTree(constructorBody));
          // Replace each super() call with `($jscomp$super$this =  || this)`
          for (Node superCall : superCalls) {
            Node newSuperCall = createNewSuperCall(superClassQName, superCall);
            superCall.replaceWith(
                IR.assign(IR.name(SUPER_THIS), IR.or(newSuperCall, IR.thisNode()))
                    .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 (declaration.isVar() && 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: " + 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.getToken() == Token.RETURN
                    && n.hasChildren()
                    && !n.getFirstChild().matchesQualifiedName("undefined")) {
                  foundNonEmptyReturn = true;
                }
              }
            }
          };
      NodeTraversal.traverseEs6(compiler, functionBody, checkForDefinedReturnValue);
      return foundNonEmptyReturn;
    }
  }
  private Node createNewSuperCall(String superClassQName, Node superCall) {
    checkArgument(superCall.isCall(), superCall);
    Node newSuperCall = superCall.cloneNode();
    Node callee = superCall.removeFirstChild();

    if (callee.isSuper()) {
      // super(...) -> SuperClass.call(this, ...)
      Node superClassDotCall =
          IR.getprop(NodeUtil.newQName(compiler, superClassQName), IR.string("call"))
              .useSourceInfoFromForTree(callee);
      newSuperCall.addChildToBack(superClassDotCall);
      newSuperCall.putBooleanProp(Node.FREE_CALL, false); // callee is now a getprop
      newSuperCall.addChildAfter(IR.thisNode().useSourceInfoFrom(callee), superClassDotCall);
    } else {
      // super.apply(null|this, ...) -> SuperClass.apply(this, ...)
      checkState(callee.isGetProp(), callee);
      Node applyNode = checkNotNull(callee.getSecondChild());
      checkState(applyNode.getString().equals("apply"), applyNode);

      newSuperCall.addChildToBack(callee);
      Node superNode = callee.getFirstChild();
      callee.replaceChild(
          superNode,
          NodeUtil.newQName(compiler, superClassQName).useSourceInfoFromForTree(superNode));
      // super.apply(null, ...) is generated by spread transpilation
      // super.apply(this, arguments) is used by Es6ConvertSuper in automatically-generated
      //   constructors.
      Node nullOrThisNode = superCall.getFirstChild();
      if (!nullOrThisNode.isThis()) {
        checkState(nullOrThisNode.isNull(), nullOrThisNode);
        superCall.removeChild(nullOrThisNode);
        newSuperCall.addChildToBack(IR.thisNode().useSourceInfoFrom(nullOrThisNode));
      }
    }
    while (superCall.hasChildren()) {
      newSuperCall.addChildToBack(superCall.removeFirstChild());
    }
    return newSuperCall;
  }

  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.isNormalBlock(), body);

    // var $jscomp$tmp$error;
    Node getError = IR.var(IR.name(TMP_ERROR)).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 = IR.assign(IR.name(TMP_ERROR), newSuperCall);
    // this.message = $jscomp$tmp$error.message,
    Node copyMessage =
        IR.assign(
            IR.getprop(IR.thisNode(), IR.string("message")),
            IR.getprop(IR.name(TMP_ERROR), IR.string("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 =
        IR.and(
            IR.in(IR.string("stack"), IR.name(TMP_ERROR)),
            IR.assign(
                IR.getprop(IR.thisNode(), IR.string("stack")),
                IR.getprop(IR.name(TMP_ERROR), IR.string("stack"))));
    Node superErrorExpr =
        IR.comma(IR.comma(IR.comma(getTmpError, copyMessage), setStack), IR.thisNode())
            .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(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 = IR.name(SUPER_THIS).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(IR.name(SUPER_THIS).useSourceInfoFrom(n)); } } }; NodeTraversal.traverseEs6(compiler, constructorBody, replaceThisWithSuperThis); } private String getSuperClassQName(Node constructor) { String className = NodeUtil.getNameNode(constructor).getQualifiedName(); Node constructorStatement = checkNotNull(NodeUtil.getEnclosingStatement(constructor)); for (Node statement = constructorStatement.getNext(); statement != null; statement = statement.getNext()) { String superClassName = getSuperClassNameIfIsInheritsStatement(statement, className); if (superClassName != null) { return superClassName; } } throw new IllegalStateException("$jscomp.inherits() call not found."); } private String getSuperClassNameIfIsInheritsStatement(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)) { Node superClass = checkNotNull(classNameNode.getNext()); return superClass.getQualifiedName(); } else { return null; } } @Override public void process(Node externs, Node root) { globalNamespace = new GlobalNamespace(compiler, externs, root); // Might need to synthesize constructors for ambient classes in .d.ts externs TranspilationPasses.processTranspile(compiler, externs, this); TranspilationPasses.processTranspile(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, this); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy