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

com.google.javascript.jscomp.CrossModuleMethodMotion 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 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 com.google.javascript.jscomp.AnalyzePrototypeProperties.NameInfo;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Property;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Symbol;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;

/**
 * Move prototype methods into later modules.
 *
 * @author [email protected] (Nick Santos)
 */
class CrossModuleMethodMotion 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 JSModuleGraph moduleGraph;
  private final boolean noStubFunctions;

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

  // Visible for testing
  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 module.
   */
  CrossModuleMethodMotion(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, false);
    this.noStubFunctions = noStubFunctions;
  }

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

  /**
   * Move methods deeper in the module 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;
      }

      JSModule 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 Property)) {
          continue;
        }
        Property prop = (Property) symbol;

        // We should only move a property across modules if:
        // 1) We can move it deeper in the module graph, and
        // 2) it's a function, and
        // 3) it is not a GETTER_DEF or a SETTER_DEF, and
        // 4) 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 module, we must
        // replace it with a stub function so that it preserves its original
        // behavior.
        if (prop.getRootVar() == null || !prop.getRootVar().isGlobal()) {
          continue;
        }

        Node value = prop.getValue();
        // 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.
            || value.getParent().isGetterDef()
            || value.getParent().isSetterDef()) {
          continue;
        }

        if (moduleGraph.dependsOn(deepestCommonModuleRef, prop.getModule())) {
          if (hasUnmovableRedeclaration(nameInfo, prop)) {
            // If it has been redeclared on the same object, skip it.
            continue;
          }

          Node valueParent = value.getParent();
          /**
           * The logic here moves methods from some starting script node to some other script node.
           * Both scripts need to be marked as changed. Locally the removal point in the starting
           * script node is called 'valueParent' and the insertion point in the destination script
           * is sometimes called 'unstubParent' and sometimes 'destParent'. The change on
           * 'valueParent' is being reported before the change occurs since the change is guaranteed
           * to occur and since after the change the 'valueParent' node has sometimes already been
           * detached.
           */
          compiler.reportChangeToEnclosingScope(valueParent);
          Node proto = prop.getPrototype();
          int stubId = idGenerator.newId();

          if (!noStubFunctions) {
            // example: JSCompiler_stubMethod(id);
            Node stubCall = IR.call(
                IR.name(STUB_METHOD_NAME),
                IR.number(stubId))
                .useSourceInfoIfMissingFromForTree(value);
            stubCall.putBooleanProp(Node.FREE_CALL, true);

            // stub out the method in the original module
            // A.prototype.b = JSCompiler_stubMethod(id);
            valueParent.replaceChild(value, stubCall);

            // unstub the function body in the deeper module
            Node unstubParent = compiler.getNodeForCodeInsertion(
                deepestCommonModuleRef);
            Node unstubCall = IR.call(
                IR.name(UNSTUB_METHOD_NAME),
                IR.number(stubId),
                value);
            unstubCall.putBooleanProp(Node.FREE_CALL, true);
            unstubParent.addChildToFront(
                // A.prototype.b = JSCompiler_unstubMethod(id, body);
                IR.exprResult(
                    IR.assign(
                        IR.getprop(
                            proto.cloneTree(),
                            IR.string(nameInfo.name)),
                        unstubCall))
                    .useSourceInfoIfMissingFromForTree(value));

            compiler.reportChangeToEnclosingScope(unstubParent);
          } else {
            Node assignmentParent = valueParent.getParent();
            valueParent.removeChild(value);
            // remove Foo.prototype.bar = value
            assignmentParent.getParent().removeChild(assignmentParent);

            Node destParent = compiler.getNodeForCodeInsertion(
                deepestCommonModuleRef);
            destParent.addChildToFront(
                // A.prototype.b = value;
                IR.exprResult(
                    IR.assign(
                        IR.getprop(
                            proto.cloneTree(),
                            IR.string(nameInfo.name)),
                        value))
                    .useSourceInfoIfMissingFromForTree(value));
            compiler.reportChangeToEnclosingScope(destParent);
          }
        }
      }
    }

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

  static boolean hasUnmovableRedeclaration(NameInfo nameInfo, Property prop) {
    for (Symbol symbol : nameInfo.getDeclarations()) {
      if (!(symbol instanceof Property)) {
        continue;
      }
      Property otherProp = (Property) symbol;
      // It is possible to do better here if the dependencies are well defined
      // but redefinitions are usually in optional modules 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;
  }

  static class IdGenerator implements Serializable {
    private static final long serialVersionUID = 0L;

    /**
     * Ids for cross-module method stubbing, so that each method has
     * a unique id.
     */
    private int currentId = 0;

    /**
     * Returns whether we've generated any new ids.
     */
    boolean hasGeneratedAnyIds() {
      return currentId != 0;
    }

    /**
     * Creates a new id for stubbing a method.
     */
    int newId() {
      return currentId++;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy