com.google.javascript.jscomp.RewritePolyfills Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.frontend.js.minifier
Show all versions of com.liferay.frontend.js.minifier
Liferay Frontend JS Minifier
/*
* 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.Function;
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.common.collect.Iterables;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.resources.ResourceLoader;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Injects polyfill libraries to ensure that ES6 library functions are available.
*/
public class RewritePolyfills implements HotSwapCompilerPass {
static final DiagnosticType INSUFFICIENT_OUTPUT_VERSION_ERROR = DiagnosticType.disabled(
"JSC_INSUFFICIENT_OUTPUT_VERSION",
"Built-in ''{0}'' not supported in output version {1}");
/**
* Represents a single polyfill: specifically, for a native symbol
* (not part of this object, but stored as the key to the map
* containing the Polyfill instance), a set of native and polyfill
* versions, and a library to ensure is injected if the output version
* is less than the native version. This is a simple value type.
*/
private static class Polyfill {
/**
* 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 no rewriting will happen.
*/
final FeatureSet 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.
*/
final FeatureSet polyfillVersion;
/**
* Runtime library to inject for the polyfill, e.g. "es6/map".
*/
final String library;
Polyfill(FeatureSet nativeVersion, FeatureSet polyfillVersion, String library) {
this.nativeVersion = nativeVersion;
this.polyfillVersion = polyfillVersion;
this.library = library;
}
}
/**
* Describes all the available polyfills, including native and
* required versions, and how to use them.
*/
static 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(Iterables.transform(statics.keySet(), EXTRACT_SUFFIX));
}
/**
* 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-impl es3 es6/array/fill
* Map es6-impl es3 es6/map
* WeakMap es6-impl es6-impl
* 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);
Polyfill polyfill =
new Polyfill(
FeatureSet.valueOf(tokens.get(1)),
FeatureSet.valueOf(tokens.get(2)),
tokens.size() > 3 ? tokens.get(3) : "");
if (symbol.contains(".prototype.")) {
methods.put(symbol.replaceAll(".*\\.prototype\\.", ""), polyfill);
} else {
statics.put(symbol, polyfill);
}
}
return new Polyfills(methods.build(), statics.build());
}
/**
* Given a qualified name {@code node}, checks whether the suffix
* of the name could possibly match a static polyfill.
*/
boolean checkSuffix(Node node) {
return node.isGetProp() ? suffixes.contains(node.getLastChild().getString())
: node.isName() ? suffixes.contains(node.getString())
: false;
}
private static final Function EXTRACT_SUFFIX =
new Function() {
@Override
public String apply(String arg) {
return arg.substring(arg.lastIndexOf('.') + 1);
}
};
}
private final AbstractCompiler compiler;
private final Polyfills polyfills;
public RewritePolyfills(AbstractCompiler compiler) {
this(
compiler,
Polyfills.fromTable(
ResourceLoader.loadTextResource(RewritePolyfills.class, "js/polyfills.txt")));
}
// Visible for testing
RewritePolyfills(AbstractCompiler compiler, Polyfills polyfills) {
this.compiler = compiler;
this.polyfills = polyfills;
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
Traverser traverser = new Traverser();
NodeTraversal.traverseEs6(compiler, scriptRoot, traverser);
if (!traverser.libraries.isEmpty()) {
Node lastNode = null;
for (String library : traverser.libraries) {
lastNode = compiler.ensureLibraryInjected(library, false);
}
if (lastNode != null) {
Node parent = lastNode.getParent();
removeUnneededPolyfills(parent, lastNode.getNext());
compiler.reportChangeToEnclosingScope(parent);
}
}
}
// Remove any $jscomp.polyfill calls whose 3rd parameter (the language version
// that already contains the library) is the same or lower than languageOut.
private void removeUnneededPolyfills(Node parent, Node runtimeEnd) {
Node node = parent.getFirstChild();
while (node != null && node != runtimeEnd) {
Node next = node.getNext();
if (NodeUtil.isExprCall(node)) {
Node call = node.getFirstChild();
Node name = call.getFirstChild();
if (name.matchesQualifiedName("$jscomp.polyfill")) {
FeatureSet nativeVersion =
FeatureSet.valueOf(name.getNext().getNext().getNext().getString());
if (languageOutIsAtLeast(nativeVersion)) {
NodeUtil.removeChild(parent, node);
}
}
}
node = next;
}
}
@Override
public void process(Node externs, Node root) {
hotSwapScript(root, null);
}
private class Traverser extends GuardedCallback {
final Set libraries = new LinkedHashSet<>();
Traverser() {
super(compiler);
}
@Override
public void visitGuarded(NodeTraversal traversal, Node node, Node parent) {
// Find qualified names that match static calls
if (node.isQualifiedName() && polyfills.checkSuffix(node)) {
String name = node.getQualifiedName();
// TODO(sdh): We could reduce some work here by combining the global names
// check with the root-in-scope check but it's not clear how to do so and
// still keep the var lookup *after* the polyfill-existence check.
boolean isExplicitGlobal = false;
for (String global : GLOBAL_NAMES) {
if (name.startsWith(global)) {
name = name.substring(global.length());
isExplicitGlobal = true;
break;
}
}
// If the name is known, then make sure it's either explicitly or implicitly global.
Polyfill polyfill = polyfills.statics.get(name);
if (polyfill != null && !isExplicitGlobal && isRootInScope(node, traversal)) {
polyfill = null;
}
if (polyfill != null && !isGuarded(name)) {
if (!languageOutIsAtLeast(polyfill.polyfillVersion)) {
traversal.report(
node,
INSUFFICIENT_OUTPUT_VERSION_ERROR,
name,
compiler.getOptions().getLanguageOut().toString());
}
inject(polyfill);
// TODO(sdh): consider warning if language_in is too low? it's not really any
// harm, and we can't do it consistently for the prototype methods, so maybe
// it's not worth doing here, either.
return; // isGetProp (below) overlaps, so just bail out now
}
}
// Inject anything that *might* match method calls - these may be removed later.
if (node.isGetProp() && node.getLastChild().isString()) {
String name = node.getLastChild().getString();
Collection methods = polyfills.methods.get(name);
if (!methods.isEmpty() && !isGuarded("." + name)) {
for (Polyfill polyfill : methods) {
inject(polyfill);
}
// NOTE(sdh): To correctly support IE8, we would need to rewrite the call site to
// e.g. $jscomp.method(foo, 'bar').call or $jscomp.call(foo, 'bar', ...args),
// which would be defined in the runtime to first check for existence (note that
// this means we can't rename that property) and then fall back on a map of
// polyfills populated by $jscomp.polyfill. This means we'd need a later
// version of this compiler pass, since the rewrite should ideally happen after
// typechecking (so that the rewrite doesn't mess it up, and we can also optionally
// not do it). For now we will pass on this, until we see concrete need. Note that
// this will not work at all in uncompiled mode, so this may be a non-starter.
}
}
return;
}
private void inject(Polyfill polyfill) {
if (!languageOutIsAtLeast(polyfill.nativeVersion) && !polyfill.library.isEmpty()) {
libraries.add(polyfill.library);
}
}
}
private static final ImmutableSet GLOBAL_NAMES =
ImmutableSet.of("goog.global.", "window.");
private boolean languageOutIsAtLeast(LanguageMode mode) {
return compiler.getOptions().getLanguageOut().compareTo(mode) >= 0;
}
private boolean languageOutIsAtLeast(FeatureSet features) {
switch (features.version()) {
case "ts":
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT6_TYPED);
case "es8":
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT_2017);
case "es7":
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT_2016);
case "es6":
case "es6-impl": // TODO(sdh): support a separate language mode for es6-impl?
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT_2015);
case "es5":
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT5);
case "es3":
return languageOutIsAtLeast(LanguageMode.ECMASCRIPT3);
default:
return false;
}
}
private static boolean isRootInScope(Node node, NodeTraversal traversal) {
Node root = NodeUtil.getRootOfQualifiedName(node);
// NOTE: `this` and `super` are always considered "in scope" and thus shouldn't be polyfilled.
return !root.isName() || traversal.getScope().getVar(root.getString()) != null;
}
}