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

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

There is a newer version: 9.0.8
Show newest version
/*
 * 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.auto.value.AutoValue;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
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.errorprone.annotations.Immutable;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TypeI;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * 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()) {
      Node parent = node.getParent();
      NodeUtil.removeChild(parent, node);
      NodeUtil.markFunctionsDeleted(node, compiler);
      compiler.reportChangeToEnclosingScope(parent);
    }
  }

  // Main traversal logic.
  private class CollectUnusedPolyfills extends GuardedCallback {

    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<>();
    // Set of all qualified name suffixes for installed polyfills, so
    // that we do not need to construct qualified names for everything.
    final Set suffixes = new HashSet<>();

    CollectUnusedPolyfills() {
      super(compiler);
    }

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

    @Override
    public void visitGuarded(NodeTraversal traversal, Node n, Node parent) {
      if (NodeUtil.isExprCall(n)) {
        Node call = n.getFirstChild();
        Node callee = call.getFirstChild();
        if (isPolyfillDefinition(callee)) {
          // A polyfill definition looks like this:
          // $jscomp.polyfill('Array.prototype.includes', ...);
          String polyfillName = call.getSecondChild().getString();
          visitPolyfillDefinition(n, polyfillName);
        }
      }
      if (n.isQualifiedName() && suffixes.contains(getLastPartOfQualifiedName(n))) {
        visitPossibleStaticPolyfillUse(n);
      }
      if (n.isGetProp()) {
        visitPossibleMethodPolyfillUse(n);
      }
    }

    // Determine if the definition is for a static or a method, and add it to the
    // appropriate "unused polyfills" map, to be removed later when a use is found.
    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);
        suffixes.add(method.method());
      } else {
        if (unusedStaticPolyfills.put(polyfillName, n) != null) {
          throw new RuntimeException(polyfillName + " polyfilled multiple times.");
        }
        suffixes.add(polyfillName.substring(polyfillName.lastIndexOf('.') + 1));
      }
    }

    // Determine if a static polyfill is being used (or if a method polyfill is being
    // used statically).  If so, remove it from the respective "unused polyfills" map.
    void visitPossibleStaticPolyfillUse(Node n) {
      String qname = removeExplicitGlobalPrefix(n.getQualifiedName());
      if (!isGuarded(qname)) {
        unusedStaticPolyfills.remove(qname);
        unusedMethodPolyfills.remove(PrototypeMethod.split(qname));
      }
    }

    // Determine if a GETPROP node could reference any polyfilled methods, now that
    // we have type information.  If so, remove any possibile matches from the
    // unusedMethodPolyfills map.
    void visitPossibleMethodPolyfillUse(Node n) {
      // Now look at the method name and possible target types.
      String methodName = n.getLastChild().getString();
      Set methods = methodsByName.get(methodName);
      if (methods.isEmpty() || isGuarded("." + methodName)) {
        return;
      }

      // Check all the methods to see if the types could possibly be compatible.
      // If so, remove from the unused methods map.
      TypeI receiverType = determineReceiverType(n);
      for (PrototypeMethod method : ImmutableSet.copyOf(methods)) {
        if (isTypeCompatible(receiverType, method.type())) {
          unusedMethodPolyfills.remove(method);
        }
      }
    }

    // Returns the type of the first child of the given node, if it's specific
    // enough to be useful for polyfill removal.  Unknown types, top, bottom,
    // and equivalent-to-object all return null, since they don't allow backing
    // off at all.
    TypeI determineReceiverType(Node n) {
      TypeI receiverType = n.getFirstChild().getTypeI();
      if (NodeUtil.isPrototypeProperty(n)) {
        TypeI maybeCtor = n.getFirstFirstChild().getTypeI();
        if (maybeCtor != null && maybeCtor.isConstructor()) {
          receiverType = maybeCtor.toMaybeFunctionType().getInstanceType();
        }
      }

      // No type information at all, return null.
      if (receiverType == null) {
        return null;
      }

      // If the known type is too generic to be useful, also return null.
      receiverType = receiverType.restrictByNotNullOrUndefined();
      if (receiverType.isUnknownType()
          || receiverType.isBottom()
          || receiverType.isTop()
          || receiverType.isEquivalentTo(
              compiler.getTypeIRegistry().getNativeType(JSTypeNative.OBJECT_TYPE))) {
        return null;
      }

      return receiverType;
    }

    // Checks whether a receiver type determined by the type checker could
    // possibly be a match for the given typename,
    boolean isTypeCompatible(TypeI receiverType, String typeName) {
      // Unknown/general types are compatible with everything.
      if (receiverType == null) {
        return true;
      }

      // Look up the typename in the registry.  All the polyfilled method
      // receiver types are built-in JS types, so they had better not be
      // missing from the registry.
      TypeI type = compiler.getTypeIRegistry().getType(typeName);
      if (type == null) {
        throw new RuntimeException("Missing built-in type: " + typeName);
      }

      // If there is any non-bottom type in common, then the types are compatible.
      if (!receiverType.meetWith(type).isBottom()) {
        return true;
      }

      // One last check - if this is a wrapped primitive type, then check the unwrapped version too.
      String primitiveType = unwrapPrimitiveWrapperTypename(typeName);
      return primitiveType != null && isTypeCompatible(receiverType, primitiveType);
    }
  }

  // Returns the final part of a qualified name, e.g. "of" from 'Array.of' and "Map" from 'Map',
  // or null for 'this' and 'super'.
  private static String getLastPartOfQualifiedName(Node n) {
    if (n.isName()) {
      return n.getString();
    } else if (n.isGetProp()) {
      return n.getLastChild().getString();
    }
    return null;
  }

  // Removes any "goog.global" (or similar) prefix from a qualified name.
  private static String removeExplicitGlobalPrefix(String qname) {
    for (String global : GLOBAL_NAMES) {
      if (qname.startsWith(global)) {
        return qname.substring(global.length());
      }
    }
    return qname;
  }

  private static final ImmutableSet GLOBAL_NAMES =
      ImmutableSet.of("goog.global.", "goog$global.", "window.");

  // Checks whether the node is (or was) a call to $jscomp.polyfill.
  private static boolean isPolyfillDefinition(Node callee) {
    // If the callee is just $jscomp.polyfill then it's easy.
    if (callee.matchesQualifiedName("$jscomp.polyfill")
        || callee.matchesQualifiedName("$jscomp$polyfill")) {
      return true;
    }
    // It's possible that the function has been inlined, so look for
    // a four-parameter function with parameters who have the correct
    // prefix (since a disambiguate suffix may have been added).
    if (callee.isFunction()) {
      Node paramList = callee.getSecondChild();
      Node param = paramList.getFirstChild();
      if (paramList.hasXChildren(4)) {
        for (String name : POLYFILL_PARAMETERS) {
          if (!param.isName() || !param.getString().startsWith(name)) {
            return false;
          }
          param = param.getNext();
        }
        return true;
      }
    }
    return false;
  }

  private static final ImmutableList POLYFILL_PARAMETERS =
      ImmutableList.of("target", "polyfill", "fromLang", "toLang");

  // Converts a wrapper type name to its primitive type, or returns null otherwise.
  private static String unwrapPrimitiveWrapperTypename(String type) {
    return PRIMITIVE_WRAPPERS.get(type);
  }

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

  // Package-private for AutoValue. Otherwise would be private.
  @AutoValue
  @Immutable
  abstract static class PrototypeMethod {
    abstract String type();
    abstract String method();

    /**
     * Builds a new PrototypeMethod from the qualified name .prototype., or returns
     * null if the qualified name does not match that pattern.
     */
    @Nullable
    static PrototypeMethod split(String name) {
      int index = name.indexOf(PROTOTYPE);
      return index < 0
          ? null
          : new AutoValue_RemoveUnusedPolyfills_PrototypeMethod(
              name.substring(0, index), name.substring(index + PROTOTYPE.length()));
    }

    @Override
    public String toString() {
      return type() + PROTOTYPE + method();
    }

    private static final String PROTOTYPE = ".prototype.";
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy