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

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

/*
 * Copyright 2008 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.AstFactory.type;

import com.google.common.annotations.VisibleForTesting;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.ClassMemberFunction;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.NameInfo;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Property;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.PrototypeProperty;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Symbol;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.Iterator;

/** Move prototype methods into later chunks. */
public class CrossChunkMethodMotion implements CompilerPass {

  // Internal errors
  static final DiagnosticType NULL_COMMON_MODULE_ERROR = DiagnosticType.error(
      "JSC_INTERNAL_ERROR_MODULE_DEPEND",
      "null deepest common module");

  private final AbstractCompiler compiler;
  private final IdGenerator idGenerator;
  private final AnalyzePrototypeProperties analyzer;
  private final JSChunkGraph moduleGraph;
  private final boolean noStubFunctions;
  private final AstFactory astFactory;

  static final String STUB_METHOD_NAME = "JSCompiler_stubMethod";
  static final String UNSTUB_METHOD_NAME = "JSCompiler_unstubMethod";

  @VisibleForTesting
  public static final String STUB_DECLARATIONS =
      "var JSCompiler_stubMap = [];"
          + "function JSCompiler_stubMethod(JSCompiler_stubMethod_id) {"
          + "  return function() {"
          + "    return JSCompiler_stubMap[JSCompiler_stubMethod_id].apply("
          + "        this, arguments);"
          + "  };"
          + "}"
          + "function JSCompiler_unstubMethod("
          + "    JSCompiler_unstubMethod_id, JSCompiler_unstubMethod_body) {"
          + "  return JSCompiler_stubMap[JSCompiler_unstubMethod_id] = "
          + "      JSCompiler_unstubMethod_body;"
          + "}";

  /**
   * Creates a new pass for moving prototype properties.
   *
   * @param compiler The compiler.
   * @param idGenerator An id generator for method stubs.
   * @param canModifyExterns If true, then we can move prototype properties that are declared in the
   *     externs file.
   * @param noStubFunctions if true, we can move methods without stub functions in the parent
   *     chunk.
   */
  CrossChunkMethodMotion(
      AbstractCompiler compiler,
      IdGenerator idGenerator,
      boolean canModifyExterns,
      boolean noStubFunctions) {
    this.compiler = compiler;
    this.idGenerator = idGenerator;
    this.moduleGraph = compiler.getModuleGraph();
    this.analyzer =
        new AnalyzePrototypeProperties(
            compiler,
            moduleGraph,
            canModifyExterns,
            /* anchorUnusedVars= */ false,
            noStubFunctions);
    this.noStubFunctions = noStubFunctions;
    this.astFactory = compiler.createAstFactory();
  }

  @Override
  public void process(Node externRoot, Node root) {
    // If there are < 2 chunks, then we will never move anything,
    // so we're done.
    if (moduleGraph.getChunkCount() > 1) {
      analyzer.process(externRoot, root);
      moveMethods(analyzer.getAllNameInfo());
    }
  }

  /**
   * Move methods deeper in the chunk graph when possible.
   */
  private void moveMethods(Collection allNameInfo) {
    boolean hasStubDeclaration = idGenerator.hasGeneratedAnyIds();
    for (NameInfo nameInfo : allNameInfo) {
      if (!nameInfo.isReferenced()) {
        // The code below can't do anything with unreferenced name
        // infos.  They should be skipped to avoid NPE since their
        // deepestCommonModuleRef is null.
        continue;
      }

      if (nameInfo.readsClosureVariables()) {
        continue;
      }

      JSChunk deepestCommonModuleRef = nameInfo.getDeepestCommonModuleRef();
      if (deepestCommonModuleRef == null) {
        compiler.report(JSError.make(NULL_COMMON_MODULE_ERROR));
        continue;
      }

      Iterator declarations =
          nameInfo.getDeclarations().descendingIterator();
      while (declarations.hasNext()) {
        Symbol symbol = declarations.next();
        if (symbol instanceof PrototypeProperty) {
          tryToMovePrototypeMethod(nameInfo, deepestCommonModuleRef, (PrototypeProperty) symbol);
        } else if (symbol instanceof ClassMemberFunction) {
          tryToMoveMemberFunction(nameInfo, deepestCommonModuleRef, (ClassMemberFunction) symbol);
        } // else it's a variable definition, and we don't move those.
      }
    }

    if (!noStubFunctions && !hasStubDeclaration && idGenerator
        .hasGeneratedAnyIds()) {
      // Declare stub functions in the top-most chunk.
      Node declarations = compiler.parseSyntheticCode("chunk_method_stubbing", STUB_DECLARATIONS);
      NodeUtil.markNewScopesChanged(declarations, compiler);
      Node firstScript = compiler.getNodeForCodeInsertion(null);
      firstScript.addChildrenToFront(declarations.removeChildren());
      compiler.reportChangeToEnclosingScope(firstScript);
    }
  }

  private void tryToMovePrototypeMethod(
      NameInfo nameInfo, JSChunk deepestCommonModuleRef, PrototypeProperty prop) {

    // We should only move a property across chunks if:
    // 1) We can move it deeper in the chunk graph, and
    // 2) it's a function, and
    // 3) it is not a GETTER_DEF or a SETTER_DEF, and
    // 4) it does not refer to `super`
    // 5) the class is available in the global scope.
    //
    // #1 should be obvious. #2 is more subtle. It's possible
    // to copy off of a prototype, as in the code:
    // for (var k in Foo.prototype) {
    //   doSomethingWith(Foo.prototype[k]);
    // }
    // This is a common way to implement pseudo-multiple inheritance in JS.
    //
    // So if we move a prototype method into a deeper chunk, we must
    // replace it with a stub function so that it preserves its original
    // behavior.
    if (prop.getRootVar() == null || !prop.getRootVar().isGlobal()) {
      return;
    }

    if (nameInfo.referencesSuper()) {
      // It is illegal to move `super` outside of a member function def.
      return;
    }

    Node value = prop.getValue();
    Node valueParent = value.getParent();
    // Only attempt to move normal functions.
    if (!value.isFunction()
        // A GET or SET can't be deferred like a normal
        // FUNCTION property definition as a mix-in would get the result
        // of a GET instead of the function itself.
        || valueParent.isGetterDef()
        || valueParent.isSetterDef()) {
      return;
    }

    if (moduleGraph.dependsOn(deepestCommonModuleRef, prop.getModule())) {
      if (hasUnmovableRedeclaration(nameInfo, prop)) {
        // If it has been redeclared on the same object, skip it.
        return;
      }
      Node destParent = compiler.getNodeForCodeInsertion(deepestCommonModuleRef);
      if (valueParent.isMemberFunctionDef()) {
        movePrototypeObjectLiteralMethodShorthand(nameInfo.name, destParent, value);
      } else if (valueParent.isStringKey()) {
        movePrototypeObjectLiteralProperty(nameInfo.name, destParent, value);
      } else {
        // Note that computed properties should have been filtered out by
        // AnalyzePrototypeProperties, because they don't have a recognizable property name.
        // Getters and setters are filtered out by the code above
        checkState(valueParent.isAssign(), valueParent);
        movePrototypeDotMethodAssignment(destParent, value);
      }
    }
  }

  /**
   * Move a property defined by object literal assigned to `.prototype`.
   *
   * 

   *     Foo.prototype = { propName: function() {}};
   * 
*/ private void movePrototypeObjectLiteralProperty( String propName, Node destParent, Node functionNode) { checkState(functionNode.isFunction(), functionNode); Node stringKey = functionNode.getParent(); checkState(stringKey.isStringKey(), stringKey); Node prototypeObjectLiteral = stringKey.getParent(); checkState(prototypeObjectLiteral.isObjectLit(), prototypeObjectLiteral); Node assignNode = prototypeObjectLiteral.getParent(); checkState( assignNode.isAssign() && prototypeObjectLiteral.isSecondChildOf(assignNode), assignNode); Node ownerDotPrototypeNode = assignNode.getFirstChild(); checkState( ownerDotPrototypeNode.isQualifiedName() && ownerDotPrototypeNode.getString().equals("prototype"), ownerDotPrototypeNode); if (noStubFunctions) { // Remove the definition from the object literal stringKey.detach(); compiler.reportChangeToEnclosingScope(prototypeObjectLiteral); // Prepend definition to new chunk // Foo.prototype.propName = function() {}; Node ownerDotPrototypeDotPropName = astFactory.createGetProp(ownerDotPrototypeNode.cloneTree(), propName, type(functionNode)); functionNode.detach(); Node definitionStatement = astFactory .createAssignStatement(ownerDotPrototypeDotPropName, functionNode) .srcrefTreeIfMissing(stringKey); destParent.addChildToFront(definitionStatement); compiler.reportChangeToEnclosingScope(destParent); } else { int stubId = idGenerator.newId(); // { propName: function() {} } => { propName: JSCompiler_stubMethod(0) } Node stubCall = createStubCall(functionNode, stubId); functionNode.replaceWith(stubCall); compiler.reportChangeToEnclosingScope(prototypeObjectLiteral); // Prepend definition to new chunk // Foo.prototype.propName = function() {}; Node ownerDotPrototypeDotPropName = astFactory.createGetProp(ownerDotPrototypeNode.cloneTree(), propName, type(functionNode)); Node unstubCall = createUnstubCall(functionNode, stubId); Node definitionStatement = astFactory .createAssignStatement(ownerDotPrototypeDotPropName, unstubCall) .srcrefTreeIfMissing(stringKey); destParent.addChildToFront(definitionStatement); compiler.reportChangeToEnclosingScope(destParent); } } /** * Move a property defined by assignment to `.prototype` or `.prototype.propName`. * *

   *     Foo.prototype.propName = function() {};
   * 
*/ private void movePrototypeDotMethodAssignment(Node destParent, Node functionNode) { checkState(functionNode.isFunction(), functionNode); Node assignNode = functionNode.getParent(); checkState(assignNode.isAssign() && functionNode.isSecondChildOf(assignNode), assignNode); Node definitionStatement = assignNode.getParent(); checkState(definitionStatement.isExprResult(), assignNode); if (noStubFunctions) { // Remove the definition statement from its current location Node assignStatementParent = definitionStatement.getParent(); definitionStatement.detach(); compiler.reportChangeToEnclosingScope(assignStatementParent); // Prepend definition to new chunk // Foo.prototype.propName = function() {}; destParent.addChildToFront(definitionStatement); compiler.reportChangeToEnclosingScope(destParent); } else { int stubId = idGenerator.newId(); // replace function definition with temporary placeholder so we can clone the whole // assignment statement without cloning the function definition itself. Node originalDefinitionPlaceholder = astFactory.createEmpty(); functionNode.replaceWith(originalDefinitionPlaceholder); Node newDefinitionStatement = definitionStatement.cloneTree(); Node newDefinitionPlaceholder = newDefinitionStatement // EXPR_RESULT .getOnlyChild() // ASSIGN .getLastChild(); // EMPTY RHS node // convert original assignment statement to // owner.prototype.propName = JSCompiler_stubMethod(0); Node stubCall = createStubCall(functionNode, stubId); originalDefinitionPlaceholder.replaceWith(stubCall); compiler.reportChangeToEnclosingScope(definitionStatement); // Prepend new definition to new chunk // Foo.prototype.propName = JSCompiler_unstubMethod(0, function() {}); Node unstubCall = createUnstubCall(functionNode, stubId); newDefinitionPlaceholder.replaceWith(unstubCall); destParent.addChildToFront(newDefinitionStatement); compiler.reportChangeToEnclosingScope(destParent); } } /** * Move a property defined by assignment to `.prototype` or `.prototype.propName`. * *

   *     Foo.prototype = { propName() {}};
   * 
*/ private void movePrototypeObjectLiteralMethodShorthand( String propName, Node destParent, Node functionNode) { checkState(functionNode.isFunction(), functionNode); Node memberFunctionDef = functionNode.getParent(); checkState(memberFunctionDef.isMemberFunctionDef(), memberFunctionDef); Node prototypeObjectLiteral = memberFunctionDef.getParent(); checkState(prototypeObjectLiteral.isObjectLit(), prototypeObjectLiteral); Node assignNode = prototypeObjectLiteral.getParent(); checkState( assignNode.isAssign() && prototypeObjectLiteral.isSecondChildOf(assignNode), assignNode); Node ownerDotPrototypeNode = assignNode.getFirstChild(); checkState( ownerDotPrototypeNode.isQualifiedName() && ownerDotPrototypeNode.getString().equals("prototype"), ownerDotPrototypeNode); if (noStubFunctions) { // Remove the definition from the object literal memberFunctionDef.detach(); compiler.reportChangeToEnclosingScope(prototypeObjectLiteral); // Prepend definition to new chunk // Foo.prototype.propName = function() {}; Node ownerDotPrototypeDotPropName = astFactory.createGetProp(ownerDotPrototypeNode.cloneTree(), propName, type(functionNode)); Node definitionStatement = astFactory .createAssignStatement(ownerDotPrototypeDotPropName, functionNode.detach()) .srcrefTreeIfMissing(memberFunctionDef); destParent.addChildToFront(definitionStatement); compiler.reportChangeToEnclosingScope(destParent); } else { int stubId = idGenerator.newId(); // { propName() {} } => { propName: JSCompiler_stubMethod(0) } Node stubCall = createStubCall(functionNode, stubId); memberFunctionDef.replaceWith(astFactory.createStringKey(propName, stubCall)); compiler.reportChangeToEnclosingScope(prototypeObjectLiteral); // Prepend definition to new chunk // Foo.prototype.propName = function() {}; Node ownerDotPrototypeDotPropName = astFactory.createGetProp(ownerDotPrototypeNode.cloneTree(), propName, type(functionNode)); Node unstubCall = createUnstubCall(functionNode.detach(), stubId); Node definitionStatement = astFactory .createAssignStatement(ownerDotPrototypeDotPropName, unstubCall) .srcrefTreeIfMissing(memberFunctionDef); destParent.addChildToFront(definitionStatement); compiler.reportChangeToEnclosingScope(destParent); } } /** * Returns a new Node to be used as the stub definition for a method. * * @param originalDefinition function Node whose definition is being stubbed * @param stubId ID to use for stubbing and unstubbing * @return a Node that looks like JSCompiler_stubMethod(0) */ private Node createStubCall(Node originalDefinition, int stubId) { return astFactory .createCall( // We can't look up the type of the stub creating method, because we add its // definition after type checking. astFactory.createNameWithUnknownType(STUB_METHOD_NAME), type(originalDefinition), astFactory.createNumber(stubId)) .srcrefTreeIfMissing(originalDefinition); } /** * Returns a new Node to be used as the stub definition for a method. * * @param functionNode actual function definition to be attached. Must be detached now. * @param stubId ID to use for stubbing and unstubbing * @return a Node that looks like JSCompiler_unstubMethod(0, function() {}) */ private Node createUnstubCall(Node functionNode, int stubId) { return astFactory .createCall( // We can't look up the type of the stub creating method, because we add its // definition after type checking. astFactory.createNameWithUnknownType(UNSTUB_METHOD_NAME), type(functionNode), astFactory.createNumber(stubId), functionNode) .srcrefTreeIfMissing(functionNode); } /** * If possible, move a class instance member function definition to the deepest chunk common to * all uses of the method. * * @param nameInfo information about all definitions of the given property name * @param deepestCommonModuleRef all uses of the method are either in this chunk or in chunks that * depend on it * @param classMemberFunction definition of the method within its class body */ private void tryToMoveMemberFunction( NameInfo nameInfo, JSChunk deepestCommonModuleRef, ClassMemberFunction classMemberFunction) { // We should only move a property across chunks if: // 1) We can move it deeper in the chunk graph, // 2) and it's a normal member function, and not a GETTER_DEF or a SETTER_DEF, or // or the class constructor. // 3) and it does not refer to `super`, which would be invalid outside of the class. // 4) and the class is available in the global scope. Var rootVar = classMemberFunction.getRootVar(); if (rootVar == null || !rootVar.isGlobal()) { return; } Node definitionNode = classMemberFunction.getDefinitionNode(); // Only attempt to move normal member functions. // A getter or setter cannot be as easily defined outside of the class to which it belongs. if (!definitionNode.isMemberFunctionDef()) { return; } if (NodeUtil.isEs6ConstructorMemberFunctionDef(definitionNode)) { // Constructor functions cannot be moved. return; } if (nameInfo.referencesSuper()) { // Do not move methods containing `super`, because it doesn't work outside of a // class method or object literal method. return; } if (moduleGraph.dependsOn(deepestCommonModuleRef, classMemberFunction.getModule())) { if (hasUnmovableRedeclaration(nameInfo, classMemberFunction)) { // If it has been redeclared on the same object, skip it. return; } Node destinationParent = compiler.getNodeForCodeInsertion(deepestCommonModuleRef); String className = rootVar.getName(); if (noStubFunctions) { moveClassInstanceMethodWithoutStub(className, definitionNode, destinationParent); } else { moveClassInstanceMethodWithStub(className, definitionNode, destinationParent); } } } private void moveClassInstanceMethodWithoutStub( String className, Node methodDefinition, Node destinationParent) { checkArgument(methodDefinition.isMemberFunctionDef(), methodDefinition); Node classMembers = checkNotNull(methodDefinition.getParent()); checkState(classMembers.isClassMembers(), classMembers); Node classNode = classMembers.getParent(); checkState(classNode.isClass(), classNode); methodDefinition.detach(); compiler.reportChangeToEnclosingScope(classMembers); // ClassName.prototype.propertyName = function() {}; Node functionNode = checkNotNull(methodDefinition.getOnlyChild()); Node classNameDotPrototypeDotPropName = astFactory.createGetProp( astFactory.createPrototypeAccess(astFactory.createName(className, type(classNode))), methodDefinition.getString(), type(functionNode)); functionNode.detach(); Node definitionStatementNode = astFactory .createAssignStatement(classNameDotPrototypeDotPropName, functionNode) .srcrefTreeIfMissing(methodDefinition); destinationParent.addChildToFront(definitionStatementNode); compiler.reportChangeToEnclosingScope(destinationParent); } private void moveClassInstanceMethodWithStub( String className, Node methodDefinition, Node destinationParent) { checkArgument(methodDefinition.isMemberFunctionDef(), methodDefinition); Node classMembers = checkNotNull(methodDefinition.getParent()); checkState(classMembers.isClassMembers(), classMembers); Node classNode = classMembers.getParent(); checkState(classNode.isClass(), classNode); int stubId = idGenerator.newId(); // Put a stub definition after the class // ClassName.prototype.propertyName = JSCompiler_stubMethod(id); Node classNameDotPrototypeDotPropName = astFactory.createGetProp( astFactory.createPrototypeAccess(astFactory.createName(className, type(classNode))), methodDefinition.getString(), type(methodDefinition)); Node stubCall = createStubCall(methodDefinition, stubId); Node stubDefinitionStatement = astFactory .createAssignStatement(classNameDotPrototypeDotPropName, stubCall) .srcrefTreeIfMissing(methodDefinition); Node classDefiningStatement = NodeUtil.getEnclosingStatement(classMembers); stubDefinitionStatement.insertAfter(classDefiningStatement); // remove the definition from the class methodDefinition.detach(); compiler.reportChangeToEnclosingScope(classMembers); // Prepend unstub definition to the new location. // ClassName.prototype.propertyName = JSCompiler_unstubMethod(id, function(...) {...}); Node classNameDotPrototypeDotPropName2 = classNameDotPrototypeDotPropName.cloneTree(); Node functionNode = checkNotNull(methodDefinition.getOnlyChild()); functionNode.detach(); Node unstubCall = createUnstubCall(functionNode, stubId); Node statementNode = astFactory .createAssignStatement(classNameDotPrototypeDotPropName2, unstubCall) .srcrefTreeIfMissing(methodDefinition); destinationParent.addChildToFront(statementNode); compiler.reportChangeToEnclosingScope(destinationParent); } static boolean hasUnmovableRedeclaration(NameInfo nameInfo, Property prop) { for (Symbol symbol : nameInfo.getDeclarations()) { if (symbol instanceof Property) { Property otherProp = (Property) symbol; // It is possible to do better here if the dependencies are well defined // but redefinitions are usually in optional chunks so it isn't likely // worth the effort to check. if (prop != otherProp && prop.getRootVar() == otherProp.getRootVar() && prop.getModule() != otherProp.getModule()) { return true; } } } return false; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy