J2clPass.java

/*
 * Copyright 2015 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.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.FunctionInjector.InliningMode;
import com.google.javascript.jscomp.FunctionInjector.Reference;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Rewrites/inlines some J2CL constructs to be more optimizable.
 */
public class J2clPass implements CompilerPass {
  private static final String ALL_CLASS_FILE_NAMES = "*";
  private final AbstractCompiler compiler;
  private final Supplier<String> safeNameIdSupplier;

  private class GetDefineRewriter extends AbstractPostOrderCallback {
    private final Set<String> defines;

    GetDefineRewriter(Set<String> defines) {
      this.defines = defines;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (isUtilGetDefineCall(n)) {
        substituteUtilGetDefine(t, n);
      }
    }

    private void substituteUtilGetDefine(NodeTraversal t, Node callNode) {
      Node firstExpr = callNode.getSecondChild();
      Node secondExpr = callNode.getLastChild();

      if (secondExpr != firstExpr) {
        secondExpr.detach();
      } else {
        // There is no secondExpr; default to null.
        secondExpr = IR.nullNode();
      }

      Node replacement = getDefineReplacement(firstExpr, secondExpr);
      replacement.useSourceInfoIfMissingFromForTree(callNode);
      callNode.replaceWith(replacement);
      t.reportCodeChange();
    }

    private Node getDefineReplacement(Node firstExpr, Node secondExpr) {
      if (defines.contains(firstExpr.getString())) {
        Node define = NodeUtil.newQName(compiler, firstExpr.getString());
        Node defineStringValue = NodeUtil.newCallNode(IR.name("String"), define);
        return IR.comma(secondExpr, defineStringValue);
      } else {
        return secondExpr;
      }
    }

    private boolean isUtilGetDefineCall(Node n) {
      return n.isCall() && isUtilGetDefineMethodName(n.getFirstChild().getQualifiedName());
    }

    private boolean isUtilGetDefineMethodName(String fnName) {
      // TODO: Switch this to the filename + property name heuristic which is less brittle.
      return fnName != null && fnName.endsWith(".$getDefine") && fnName.contains("Util");
    }
  }


  /**
   * Collects references to certain function definitions in a certain class and then inlines fully
   * qualified static method calls to those functions anywhere in the program.
   *
   * <p>Assumes that the set of provided short function names will not collide with any of the
   * collected fully qualified function names once the module prefix has been added.
   */
  private class ClassStaticFunctionsInliner {
    private final String classFileName;
    private final Set<String> fnNamesToInline;
    private final InliningMode inliningMode;
    private final Map<String, Node> fnsToInlineByQualifiedName = new HashMap<>();
    private final FunctionInjector injector;
    private final Node root;

    private ClassStaticFunctionsInliner(
        Node root, String classFileName, Set<String> fnNamesToInline, InliningMode inliningMode) {
      this.root = root;
      this.classFileName = classFileName;
      this.fnNamesToInline = fnNamesToInline;
      this.inliningMode = inliningMode;

      this.injector = new FunctionInjector(compiler, safeNameIdSupplier, true, true, true);
      this.injector.setKnownConstants(fnNamesToInline);
    }

    private void run() {
      NodeTraversal.traverseEs6(compiler, root, new FunctionDefsCollector());
      NodeTraversal.traverseEs6(compiler, root, new StaticCallInliner());
    }

    private class FunctionDefsCollector implements Callback {
      @Override
      public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
        // Only look inside the referenced class file.
        return !n.isScript()
            || n.getSourceFileName().endsWith(classFileName)
            || classFileName.equals(ALL_CLASS_FILE_NAMES);
      }

      @Override
      public void visit(NodeTraversal t, Node n, Node parent) {
        // If we arrive here then we're already inside the desired script.

        // Only look at named function declarations
        if (!n.isAssign() || !n.getLastChild().isFunction()) {
          return;
        }

        // ... that are fully qualified
        Node qualifiedNameNode = n.getFirstChild();
        if (!qualifiedNameNode.isGetProp() || !qualifiedNameNode.isQualifiedName()) {
          return;
        }

        Node fnNode = n.getLastChild();
        String qualifiedFnName = qualifiedNameNode.getQualifiedName();
        String fnName = qualifiedNameNode.getLastChild().getString();
        if (fnNamesToInline.contains(fnName)) {
          // Then store a reference to it.
          fnsToInlineByQualifiedName.put(qualifiedFnName, fnNode);
        }
      }
    }

    private class StaticCallInliner extends AbstractPostOrderCallback {
      @Override
      public void visit(NodeTraversal t, Node n, Node parent) {
        // Only look at method calls
        if (!n.isCall()) {
          return;
        }

        // ... that are fully qualified
        Node qualifiedNameNode = n.getFirstChild();
        if (!qualifiedNameNode.isGetProp() || !qualifiedNameNode.isQualifiedName()) {
          return;
        }

        // ... and that reference a function definition we want to inline
        String qualifiedFnName = qualifiedNameNode.getQualifiedName();
        String fnName = qualifiedNameNode.getLastChild().getString();
        Node fnImpl = fnsToInlineByQualifiedName.get(qualifiedFnName);
        if (fnImpl == null) {
          return;
        }

        // Otherwise inline the call.
        Node inlinedCall =
            injector.inline(
                new Reference(n, t.getScope(), t.getModule(), inliningMode), fnName, fnImpl);
        t.getCompiler().reportChangeToEnclosingScope(inlinedCall);
      }
    }
  }

  public J2clPass(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.safeNameIdSupplier = compiler.getUniqueNameIdSupplier();
  }

  @Override
  public void process(Node externs, Node root) {
    if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
      return;
    }

    /*
     * Re-writes Util.getDefine to make it work for compiled mode.
     */
    Set<String> defines =
        new ProcessDefines(compiler, null, true).collectDefines(externs, root).keySet();
    NodeTraversal.traverseEs6(compiler, root, new GetDefineRewriter(defines));

    /*
     * Inlines Arrays.$create(), Arrays.$init(), Arrays.$instanceIsOfType(), Arrays.$castTo() and
     * Casts.$to() so that all references to Object.$isInstance() functions will be fully qualified
     * and easy to strip.
     */
    inlineFunctionsInFile(
        root,
        "Arrays.impl.java.js",
        ImmutableSet.of("$create", "$init", "$instanceIsOfType", "$castTo", "$stampType"),
        InliningMode.DIRECT);
    inlineFunctionsInFile(
        root, "Casts.impl.java.js", ImmutableSet.of("$to"), InliningMode.DIRECT);

    /*
     * Inlines all Interface.$markImplementor(FooClass) metaclass calls so that FooClass and others
     * like it are not unnecessarily retained and so that static analysis of interface instanceof
     * calls becomes possible.
     *
     * Note that his pass should NOT be restricted to j2cl .java.js files because JavaScript code
     * implementing Java interfaces (not recommended but widely used in xplat) needs calls to
     * $markImplementor.
     *
     */
    inlineFunctionsInFile(
        root, ALL_CLASS_FILE_NAMES, ImmutableSet.of("$markImplementor"), InliningMode.BLOCK);

    /*
     * Inlines class metadata calls so they become optimizable and avoids escaping of constructor.
     */
    inlineFunctionsInFile(
        root,
        "Util.impl.java.js",
        ImmutableSet.of(
            "$setClassMetadata",
            "$setClassMetadataForInterface",
            "$setClassMetadataForEnum",
            "$setClassMetadataForPrimitive"),
        InliningMode.BLOCK);
  }

  private void inlineFunctionsInFile(
      Node root, String classFileName, Set<String> fnNamesToInline, InliningMode inliningMode) {
    new ClassStaticFunctionsInliner(root, classFileName, fnNamesToInline, inliningMode).run();
  }
}