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

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

/*
 * Copyright 2020 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 static com.google.common.base.Preconditions.checkArgument;

import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/** Detects all potential usages of polyfilled classes or methods */
final class PolyfillUsageFinder {

  /**
   * Represents a single polyfill: specifically, for a native symbol, a set of native and polyfill
   * versions, and a library to ensure is injected if the output version is less than the native
   * version.
   */
  static final class Polyfill {
    /** The full name of the polyfill, e.g `Map` or `String.prototype.includes` */
    final String nativeSymbol;

    /**
     * The language version at (or above) which the native symbol is available and sufficient. If
     * the language out flag is at least as high as {@code nativeVersion} then the polyfill is not
     * needed. This string should be one of those returned by FeatureSet.version().
     */
    final String nativeVersion;

    /**
     * The required language version for the polyfill to work. This should not be higher than {@code
     * nativeVersion}, but may be the same in cases where there is no polyfill provided. This is
     * used to emit a warning if the language out flag is too low. This string should be one of
     * those returned by FeatureSet.version().
     */
    final String polyfillVersion;

    /**
     * Runtime library to inject for the polyfill, e.g. "es6/map".
     */
    final String library;

    final Kind kind;

    enum Kind {
      STATIC, // Map or Array.of
      METHOD // String.prototype.includes
    }

    Polyfill(
        String nativeSymbol,
        String nativeVersion,
        String polyfillVersion,
        String library,
        Kind kind) {
      this.nativeSymbol = nativeSymbol;
      this.nativeVersion = nativeVersion;
      this.polyfillVersion = polyfillVersion;
      this.library = library;
      this.kind = kind;
    }
  }

  /** Maps from polyfill names to the actual Polyfill object. */
  static final class Polyfills {
    // Map of method polyfills, keyed by native method name.
    private final ImmutableMultimap methods;
    // Map of static polyfills, keyed by fully-qualified native name.
    private final ImmutableMap statics;
    // Set of suffixes of qualified names.
    private final ImmutableSet suffixes;

    private Polyfills(
        ImmutableMultimap methods, ImmutableMap statics) {
      this.methods = methods;
      this.statics = statics;
      this.suffixes =
          ImmutableSet.copyOf(
              statics.keySet().stream()
                  .map(arg -> arg.substring(arg.lastIndexOf('.') + 1))
                  .collect(Collectors.toList()));
    }

    /**
     * Builds a Polyfills instance from a polyfill table, which is a simple
     * text file with lines containing space-separated tokens:
     *   [NATIVE_SYMBOL] [NATIVE_VERSION] [POLYFILL_VERSION] [LIBRARY]
     * For example,
     *   Array.prototype.fill es6 es3 es6/array/fill
     *   Map es6 es3 es6/map
     *   WeakMap es6 es6
     * The last line, WeakMap, does not have a polyfill available, so the
     * library token is empty.
     */
    static Polyfills fromTable(String table) {
      ImmutableMultimap.Builder methods = ImmutableMultimap.builder();
      ImmutableMap.Builder statics = ImmutableMap.builder();
      for (String line : Splitter.on('\n').omitEmptyStrings().split(table)) {
        List tokens = Splitter.on(' ').omitEmptyStrings().splitToList(line.trim());
        if (tokens.size() == 1 && tokens.get(0).isEmpty()) {
          continue;
        } else if (tokens.size() < 3) {
          throw new IllegalArgumentException("Invalid table: too few tokens on line: " + line);
        }
        String symbol = tokens.get(0);
        boolean isPrototypeMethod = symbol.contains(".prototype.");
        final String nativeVersionStr = tokens.get(1);
        final String polyfillVersionStr = tokens.get(2);
        Polyfill polyfill =
            new Polyfill(
                symbol,
                nativeVersionStr,
                polyfillVersionStr,
                tokens.size() > 3 ? tokens.get(3) : "",
                isPrototypeMethod ? Polyfill.Kind.METHOD : Polyfill.Kind.STATIC);
        if (isPrototypeMethod) {
          methods.put(symbol.replaceAll(".*\\.prototype\\.", ""), polyfill);
        } else {
          statics.put(symbol, polyfill);
        }
      }
      return new Polyfills(methods.build(), statics.build());
    }

  }

  @AutoValue
  abstract static class PolyfillUsage {
    abstract Polyfill polyfill();

    abstract Node node();

    abstract String name();

    abstract boolean isExplicitGlobal();

    private static PolyfillUsage createExplicit(Polyfill polyfill, Node node, String name) {
      return new AutoValue_PolyfillUsageFinder_PolyfillUsage(
          polyfill, node, name, /* isExplicitGlobal= */ true);
    }

    private static PolyfillUsage createNonExplicit(Polyfill polyfill, Node node, String name) {
      return new AutoValue_PolyfillUsageFinder_PolyfillUsage(
          polyfill, node, name, /* isExplicitGlobal= */ false);
    }
  }

  private final AbstractCompiler compiler;
  private final Polyfills polyfills;

  PolyfillUsageFinder(AbstractCompiler compiler, Polyfills polyfills) {
    this.polyfills = polyfills;
    this.compiler = compiler;
  }

  /**
   * Passes all polyfill usages found, in postorder, to the given polyfillConsumer
   *
   * 

Excludes polyfill usages behind a guard, like {@code if (Promise) return * Promise.resolve('ok');} */ void traverseExcludingGuarded(Node root, Consumer polyfillConsumer) { NodeTraversal.traverse( compiler, root, new Traverser(this.compiler, polyfillConsumer, Guard.ONLY_UNGUARDED)); } /** * Passes all polyfill usages found, in postorder, to the given polyfillConsumer * *

Includes polyfill usages that are behind a guard, like {@code if (Promise) return * Promise.resolve('ok');} */ void traverseIncludingGuarded(Node root, Consumer polyfillConsumer) { NodeTraversal.traverse( compiler, root, new Traverser(this.compiler, polyfillConsumer, Guard.ALL)); } /** * Passes all polyfill usages found, in postorder, to the given polyfillConsumer * *

Only includes polyfill usages that are behind a guard, like {@code if (Promise) return * Promise.resolve('ok');} */ void traverseOnlyGuarded(Node root, Consumer polyfillConsumer) { NodeTraversal.traverse( compiler, root, new Traverser(this.compiler, polyfillConsumer, Guard.ONLY_GUARDED)); } private enum Guard { ONLY_GUARDED, ONLY_UNGUARDED, ALL; boolean shouldInclude(boolean isGuarded) { switch (this) { case ALL: return true; case ONLY_GUARDED: return isGuarded; case ONLY_UNGUARDED: return !isGuarded; } throw new AssertionError(); } }; private class Traverser extends GuardedCallback { private final Consumer polyfillConsumer; // Whether to emit usages like Promise in `if (Promise) return Promise.resolve('ok');}` private final Guard includeGuardedUsages; Traverser( AbstractCompiler compiler, Consumer polyfillConsumer, Guard includeGuardedUsages) { super(compiler); this.polyfillConsumer = polyfillConsumer; this.includeGuardedUsages = includeGuardedUsages; } @Override public void visitGuarded(NodeTraversal traversal, Node node, Node parent) { switch (node.getToken()) { case NAME: visitName(traversal, node); break; case GETPROP: case OPTCHAIN_GETPROP: visitGetPropChain(traversal, node); break; default: // nothing to do } } private void visitName(NodeTraversal traversal, Node nameNode) { String name = nameNode.getString(); Polyfill polyfill = polyfills.statics.get(name); if (polyfill == null) { // no polyfill exists for this name return; } if (traversal.getScope().getVar(name) != null) { // This class is only supposed to traverse over actual sources, not externs, // so it shouldn't see the declarations of the things being polyfilled. // If we see a declaration of the name, then it is defined by the source code, // and we won't count it as a reference to our polyfill. return; } if (includeGuardedUsages.shouldInclude(isGuarded(name))) { this.polyfillConsumer.accept(PolyfillUsage.createNonExplicit(polyfill, nameNode, name)); } } private void visitGetPropChain(NodeTraversal traversal, Node getPropNode) { // First see if we have a usage that matches a full static polyfill name. // e.g. `Array.from` or `globalThis.Promise.allSettled` PolyfillUsage staticPolyfillUsage = maybeCreateStaticPolyfillUsageForGetPropChain(traversal, getPropNode); if (staticPolyfillUsage != null) { if (includeGuardedUsages.shouldInclude(isGuarded(staticPolyfillUsage.name()))) { this.polyfillConsumer.accept(staticPolyfillUsage); } } else { // We don't have a static polyfill usage, but this could still be a reference to one of // several possible method polyfills. // e.g. `obj.includes(x)` could be a usage of `Array.prototype.includes` or // `String.prototype.includes`. final String propertyName = getPropNode.getSecondChild().getString(); Collection methodPolyfills = polyfills.methods.get(propertyName); // Note that we use ".foo" as the guard check for methods to keep them distinct in case // there is also a static "foo" polyfill. if (!methodPolyfills.isEmpty() && includeGuardedUsages.shouldInclude(isGuarded("." + propertyName))) { for (Polyfill polyfill : methodPolyfills) { this.polyfillConsumer.accept( PolyfillUsage.createNonExplicit(polyfill, getPropNode, propertyName)); } } } } } @Nullable private PolyfillUsage maybeCreateStaticPolyfillUsageForGetPropChain( NodeTraversal traversal, final Node getPropNode) { checkArgument(getPropNode.isGetProp() || getPropNode.isOptChainGetProp(), getPropNode); final String lastComponent = getPropNode.getSecondChild().getString(); if (!polyfills.suffixes.contains(lastComponent)) { // Save execution time by bailing out early if the property name at the end of the chain // doesn't match any of the known polyfills. return null; } // NOTE: We are not using isQualifiedName() and getQualifiedName() here, because we want to // locate the owner node and also have this code work for optional chains. final ArrayDeque components = new ArrayDeque<>(); components.addFirst(lastComponent); Node ownerNode; for (ownerNode = getPropNode.getFirstChild(); ownerNode.isGetProp() || ownerNode.isOptChainGetProp(); ownerNode = ownerNode.getFirstChild()) { components.addFirst(ownerNode.getSecondChild().getString()); } if (!ownerNode.isName()) { // Static polyfills are always fully qualified names beginning with a NAME node. // e.g. `Array.from` or `globalThis.Promise` return null; } final String rootName = ownerNode.getString(); components.addFirst(rootName); final String fullName = String.join(".", components); final String globalPrefix = findGlobalPrefix(fullName); if (globalPrefix != null) { // The full name starts with a known global value, like `goog.global.` or `globalThis.`. // We must strip that off before matching with the known polyfill names. // (Note that the connecting '.' is included and will also be stripped.) // Also, the presence of the explicit global value name means we don't have to check the // scope for a shadowing variable as we do below. Polyfill polyfill = polyfills.statics.get(fullName.substring(globalPrefix.length())); if (polyfill != null) { return PolyfillUsage.createExplicit(polyfill, getPropNode, polyfill.nativeSymbol); } } else { Polyfill polyfill = polyfills.statics.get(fullName); if (polyfill != null) { // This class is only supposed to traverse over actual sources, not externs, // so it shouldn't see the declarations of the things being polyfilled. // If we see a declaration of the name, then it is defined by the source code, // and we won't count it as a reference to our polyfill. // Checking the scope is relatively expensive, so we don't want to do it until // we've confirmed that this node looks like it could be a polyfill reference. if (traversal.getScope().getVar(rootName) == null) { return PolyfillUsage.createNonExplicit(polyfill, getPropNode, polyfill.nativeSymbol); } } } return null; } @Nullable private static String findGlobalPrefix(String qualifiedName) { for (String global : GLOBAL_NAMES) { if (qualifiedName.startsWith(global)) { return global; } } return null; } private static final ImmutableSet GLOBAL_NAMES = ImmutableSet.of("goog.global.", "window.", "goog$global.", "globalThis."); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy