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

com.google.javascript.jscomp.RemoveUnusedPolyfills 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 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.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.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.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()) {
      Node parent = node.getParent();
      NodeUtil.removeChild(parent, node);
      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");

  // Simple value type for a (type,method) pair.
  private static class PrototypeMethod {
    // Builds a new PrototypeMethod from the qualified name .prototype.,
    // or returns null if the qualified name does not match that pattern.
    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()));
    }

    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;
    }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy