-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
RewritePolyfills.java
322 lines (287 loc) · 12.2 KB
/
RewritePolyfills.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/*
* 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.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}: set --language_out to at least {2}");
/**
* 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<String, Polyfill> methods;
// Map of static polyfills, keyed by fully-qualified native name.
private final ImmutableMap<String, Polyfill> statics;
// Set of suffixes of qualified names.
private final ImmutableSet<String> suffixes;
private Polyfills(
ImmutableMultimap<String, Polyfill> methods, ImmutableMap<String, Polyfill> 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<String, Polyfill> methods = ImmutableMultimap.builder();
ImmutableMap.Builder<String, Polyfill> statics = ImmutableMap.builder();
for (String line : Splitter.on('\n').omitEmptyStrings().split(table)) {
List<String> 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<String, String> EXTRACT_SUFFIX =
new Function<String, String>() {
@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<String> {
final Set<String> 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(),
polyfill.polyfillVersion.toLanguageModeString());
}
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<Polyfill> 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<String> 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;
}
}