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

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

/*
 * 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.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.SetMultimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TypeI;
import com.google.javascript.rhino.TypeIRegistry;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Removes any unused polyfill instance methods, using type information to
 * disambiguate calls.  This is a separate pass from {@link RewritePolyfills}
 * because once optimization has started it's not feasible to inject any
 * further runtime libraries, since they're all inter-related.  Thus, the
 * initial polyfill pass is very liberal in the polyfills it adds.  This
 * pass prunes the cases where the type checker can verify that the polyfill
 * was not actually needed.
 *
 * It would be great if we didn't need a special-case optimization for this,
 * i.e. if polyfill injection could be delayed until after the first pass of
 * {@link SmartNameRemoval}, but this causes problems with earlier-injected
 * runtime libraries having already had their properties collapsed, so that
 * later-injected polyfills can no longer reference these names correctly.
 */
class RemoveUnusedPolyfills implements CompilerPass {

  private final AbstractCompiler compiler;

  RemoveUnusedPolyfills(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    CollectUnusedPolyfills collector = new CollectUnusedPolyfills();
    NodeTraversal.traverseEs6(compiler, root, collector);
    for (Node node : collector.removableNodes()) {
      NodeUtil.removeChild(node.getParent(), node);
      compiler.reportCodeChange();
    }
  }

  private static final ImmutableMap PRIMITIVE_WRAPPERS = ImmutableMap.of(
      "Boolean", "boolean",
      "Number", "number",
      "String", "string");

  private static final String GOOG_GLOBAL = "goog.global.";
  private static final String WINDOW = "window.";

  private class CollectUnusedPolyfills extends AbstractPostOrderCallback {

    final SetMultimap methodsByName = HashMultimap.create();
    // These maps map polyfill names to their definitions in the AST.
    // Each polyfill is considered unused by default, and if we find uses of it we
    // remove it from these maps.
    final Map unusedMethodPolyfills = new HashMap<>();
    final Map unusedStaticPolyfills = new HashMap<>();

    Iterable removableNodes() {
      return Iterables.concat(unusedMethodPolyfills.values(), unusedStaticPolyfills.values());
    }

    @Override
    public void visit(NodeTraversal traversal, Node n, Node parent) {
      if (NodeUtil.isExprCall(n)) {
        Node call = n.getFirstChild();
        Node callee = call.getFirstChild();
        String originalName = callee.getOriginalQualifiedName();
        if ("$jscomp.polyfill".equals(originalName)) {
          // A polyfill definition looks like this:
          // $jscomp.polyfill('Array.prototype.includes', ...);
          String polyfillName = call.getSecondChild().getString();
          visitPolyfillDefinition(n, polyfillName);
        }
      } else if (n.isGetProp() || n.isQualifiedName()) {
        visitPossiblePolyfillUse(n);
      }
    }

    void visitPolyfillDefinition(Node n, String polyfillName) {
      // Find the $jscomp.polyfill calls and add them to the table.
      PrototypeMethod method = PrototypeMethod.split(polyfillName);
      if (method != null) {
        if (unusedMethodPolyfills.put(method, n) != null) {
          throw new RuntimeException(method + " polyfilled multiple times.");
        }
        methodsByName.put(method.method, method);
      } else {
        if (unusedStaticPolyfills.put(polyfillName, n) != null) {
          throw new RuntimeException(polyfillName + " polyfilled multiple times.");
        }
      }
    }

    void visitPossiblePolyfillUse(Node n) {
      // Remove anything from the table that could possibly be needed.
      if (n.isQualifiedName()) {
        // First remove anything with an exact qualified name match.
        String qname = n.getQualifiedName();
        if (qname.startsWith(GOOG_GLOBAL)) {
          qname = qname.substring(GOOG_GLOBAL.length());
        } else if (qname.startsWith(WINDOW)) {
          qname = qname.substring(WINDOW.length());
        }
        unusedStaticPolyfills.remove(qname);
        unusedMethodPolyfills.remove(PrototypeMethod.split(qname));
      }
      if (!n.isGetProp()) {
        return;
      }
      // Now look at the method name and possible target types.
      String methodName = n.getLastChild().getString();
      Set methods = methodsByName.get(methodName);
      if (methods.isEmpty()) {
        return;
      }
      TypeI receiverType = n.getFirstChild().getTypeI();
      if (NodeUtil.isPrototypeProperty(n)) {
        TypeI maybeCtor = n.getFirstFirstChild().getTypeI();
        if (maybeCtor != null && maybeCtor.isConstructor()) {
          receiverType = maybeCtor.toMaybeFunctionType().getInstanceType();
        }
      }
      if (receiverType == null) {
        // TODO(sdh): When does this happen?  If it means incomplete type information, then
        // we need to remove all the potential methods.  If not, we can just return.
        unusedMethodPolyfills.keySet().removeAll(methods);
        return;
      }
      receiverType = receiverType.restrictByNotNullOrUndefined();
      TypeIRegistry registry = compiler.getTypeIRegistry();
      if (receiverType.isUnknownType()
          || receiverType.isBottom()
          || receiverType.isTop()
          || receiverType.isEquivalentTo(
              registry.getNativeType(JSTypeNative.OBJECT_TYPE))) {
        unusedMethodPolyfills.keySet().removeAll(methods);
      }
      for (PrototypeMethod method : ImmutableSet.copyOf(methods)) {
        checkType(receiverType, registry, method, method.type);
        String primitiveType = PRIMITIVE_WRAPPERS.get(method.type);
        if (primitiveType != null) {
          checkType(receiverType, registry, method, primitiveType);
        }
      }
    }

    private void checkType(
        TypeI receiverType, TypeIRegistry registry, PrototypeMethod method, String typeName) {
      TypeI type = registry.getType(typeName);
      if (type == null) {
        throw new RuntimeException("Missing built-in type: " + typeName);
      }
      if (!receiverType.meetWith(type).isBottom()) {
        unusedMethodPolyfills.remove(method);
      }
    }
  }

  // Simple value type for a (type,method) pair.
  private static class PrototypeMethod {
    final String type;
    final String method;
    PrototypeMethod(String type, String method) {
      this.type = type;
      this.method = method;
    }
    @Override public boolean equals(Object other) {
      return other instanceof PrototypeMethod
          && ((PrototypeMethod) other).type.equals(type)
          && ((PrototypeMethod) other).method.equals(method);
    }
    @Override public int hashCode() {
      return Objects.hash(type, method);
    }
    @Override public String toString() {
      return type + PROTOTYPE + method;
    }
    static PrototypeMethod split(String name) {
      int index = name.indexOf(PROTOTYPE);
      return index < 0
          ? null
          : new PrototypeMethod(
                name.substring(0, index), name.substring(index + PROTOTYPE.length()));
    }
  }
  private static final String PROTOTYPE = ".prototype.";
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy