Skip to content

Commit a5abd23

Browse files
lauraharkercopybara-github
authored andcommitted
Add CompilerOptions.setInjectPolyfillsNewerThan
PiperOrigin-RevId: 723255441
1 parent 0b7b374 commit a5abd23

File tree

7 files changed

+216
-34
lines changed

7 files changed

+216
-34
lines changed

src/com/google/javascript/jscomp/CompilerOptions.java

+30
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,28 @@ public static enum OutputJs {
862862
/** Isolates injected polyfills from the global scope. */
863863
private boolean isolatePolyfills = false;
864864

865+
/**
866+
* Configures polyfill injection of all polyfills newer than the given language mode, regardless
867+
* of whether they are actually referenced in the code being compiled.
868+
*
869+
* <p>How is this different from --inject_library? --inject_library takes a specific JS file
870+
* defined in the jscomp/js package, and injects that and all of its dependencies. This option
871+
* takes a language mode.
872+
*
873+
* <p>For example, the common use of `--inject_library` is passing `--inject_library=es6_runtime`.
874+
* es6_runtime.js is a file that contains both all the ES6 polyfills and also ES6 transpilation
875+
* utilities (library code referenced by transpiled code). So
876+
*
877+
* <ul>
878+
* <li>es6_runtime includes transpilation utilities, not just polyfills.
879+
* `--inject_polyfills_newer_than=ES5` will not add transpilation utilities, just the
880+
* polyfills,
881+
* <li>if someone forgot to add a new polyfill to es6_runtime.js, then `--inject_library` will
882+
* not add it, but `--inject_polyfills_newer_than=ES5` will add it.
883+
* </ul>
884+
*/
885+
private LanguageMode injectPolyfillsNewerThan = null;
886+
865887
/** Whether to instrument reentrant functions for AsyncContext. */
866888
private boolean instrumentAsyncContext = false;
867889

@@ -2610,6 +2632,14 @@ public boolean getIsolatePolyfills() {
26102632
return this.isolatePolyfills;
26112633
}
26122634

2635+
public void setInjectPolyfillsNewerThan(LanguageMode injectPolyfillsNewerThan) {
2636+
this.injectPolyfillsNewerThan = injectPolyfillsNewerThan;
2637+
}
2638+
2639+
LanguageMode getInjectPolyfillsNewerThan() {
2640+
return this.injectPolyfillsNewerThan;
2641+
}
2642+
26132643
/** Sets whether to isolate polyfills from the global scope. */
26142644
public void setInstrumentAsyncContext(boolean instrumentAsyncContext) {
26152645
this.instrumentAsyncContext = instrumentAsyncContext;

src/com/google/javascript/jscomp/DefaultPassConfig.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,14 @@ protected PassListBuilder getTranspileOnlyPasses() {
154154

155155
TranspilationPasses.addTranspilationRuntimeLibraries(passes);
156156

157-
if (options.needsTranspilationFrom(ES2015)) {
158-
if (options.getRewritePolyfills()) {
157+
if (options.needsTranspilationFrom(ES2015) && options.getRewritePolyfills()) {
159158
if (options.getIsolatePolyfills()) {
160159
throw new IllegalStateException(
161160
"Polyfill isolation cannot be used in transpileOnly mode");
162161
}
163162
TranspilationPasses.addRewritePolyfillPass(passes);
164-
}
163+
} else if (options.getInjectPolyfillsNewerThan() != null) {
164+
TranspilationPasses.addRewritePolyfillPass(passes);
165165
}
166166

167167
passes.maybeAdd(injectRuntimeLibraries);
@@ -883,7 +883,9 @@ private PassListBuilder getEarlyOptimizationPasses() {
883883

884884
TranspilationPasses.addTranspilationRuntimeLibraries(passes);
885885

886-
if (options.rewritePolyfills || options.getIsolatePolyfills()) {
886+
if (options.rewritePolyfills
887+
|| options.getIsolatePolyfills()
888+
|| options.getInjectPolyfillsNewerThan() != null) {
887889
TranspilationPasses.addRewritePolyfillPass(passes);
888890
}
889891

@@ -2392,7 +2394,9 @@ public void process(Node externs, Node root) {
23922394
// If we are forcing injection of some library code, don't remove polyfills.
23932395
// Otherwise, we might end up removing polyfills the user specifically asked
23942396
// to include.
2395-
.removeUnusedPolyfills(options.forceLibraryInjection.isEmpty())
2397+
.removeUnusedPolyfills(
2398+
options.forceLibraryInjection.isEmpty()
2399+
&& options.getInjectPolyfillsNewerThan() == null)
23962400
.assumeGettersArePure(options.getAssumeGettersArePure())
23972401
.build())
23982402
.build();

src/com/google/javascript/jscomp/PolyfillUsageFinder.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121
import com.google.auto.value.AutoValue;
2222
import com.google.common.base.Splitter;
2323
import com.google.common.collect.ImmutableCollection;
24+
import com.google.common.collect.ImmutableList;
2425
import com.google.common.collect.ImmutableMap;
2526
import com.google.common.collect.ImmutableMultimap;
2627
import com.google.common.collect.ImmutableSet;
2728
import com.google.javascript.jscomp.AbstractCompiler.LifeCycleStage;
29+
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
30+
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
31+
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
2832
import com.google.javascript.rhino.Node;
2933
import java.util.ArrayDeque;
3034
import java.util.List;
@@ -93,11 +97,17 @@ static final class Polyfills {
9397
private final ImmutableMap<String, Polyfill> statics;
9498
// Set of suffixes of qualified names.
9599
private final ImmutableSet<String> suffixes;
100+
// Map of all polyfills, keyed by their native version (the first ECMAScript spec version in
101+
// which they are defined)
102+
private final ImmutableMultimap<String, Polyfill> byNativeVersion;
96103

97104
private Polyfills(
98-
ImmutableMultimap<String, Polyfill> methods, ImmutableMap<String, Polyfill> statics) {
105+
ImmutableMultimap<String, Polyfill> methods,
106+
ImmutableMap<String, Polyfill> statics,
107+
ImmutableMultimap<String, Polyfill> byNativeVersion) {
99108
this.methods = methods;
100109
this.statics = statics;
110+
this.byNativeVersion = byNativeVersion;
101111
this.suffixes =
102112
ImmutableSet.copyOf(
103113
statics.keySet().stream()
@@ -119,6 +129,7 @@ private Polyfills(
119129
static Polyfills fromTable(String table) {
120130
ImmutableMultimap.Builder<String, Polyfill> methods = ImmutableMultimap.builder();
121131
ImmutableMap.Builder<String, Polyfill> statics = ImmutableMap.builder();
132+
ImmutableMultimap.Builder<String, Polyfill> byNativeVersion = ImmutableMultimap.builder();
122133
for (String line : Splitter.on('\n').omitEmptyStrings().split(table)) {
123134
List<String> tokens = Splitter.on(' ').omitEmptyStrings().splitToList(line.trim());
124135
if (tokens.size() == 1 && tokens.get(0).isEmpty()) {
@@ -142,10 +153,46 @@ static Polyfills fromTable(String table) {
142153
} else {
143154
statics.put(symbol, polyfill);
144155
}
156+
byNativeVersion.put(nativeVersionStr, polyfill);
145157
}
146-
return new Polyfills(methods.build(), statics.buildOrThrow());
158+
return new Polyfills(methods.build(), statics.buildOrThrow(), byNativeVersion.build());
147159
}
148160

161+
ImmutableList<Polyfill> getPolyfillsNewerThan(LanguageMode languageMode) {
162+
FeatureSet featureSet = languageMode.toFeatureSet();
163+
ImmutableList.Builder<Polyfill> result = ImmutableList.builder();
164+
165+
for (String nativeVersionStr : byNativeVersion.keySet()) {
166+
FeatureSet polyfillNativeFeatureSet = getPolyfillSupportedFeatureSet(nativeVersionStr);
167+
if (!featureSet.contains(polyfillNativeFeatureSet)) {
168+
result.addAll(byNativeVersion.get(nativeVersionStr));
169+
}
170+
}
171+
return result.build();
172+
}
173+
}
174+
175+
/**
176+
* Converts a polyfill native version string as passed to $jscomp.polyfill, e.g. "es6", to a
177+
* FeatureSet.
178+
*/
179+
static FeatureSet getPolyfillSupportedFeatureSet(String nativeVersionStr) {
180+
FeatureSet polyfillSupportFeatureSet = FeatureSet.valueOf(nativeVersionStr);
181+
// Safari has been really slow to implement these regex features, even though it has
182+
// kept on top of the features we polyfill, so we want to ignore the regex features
183+
// when deciding whether the polyfill should be considered "already supported" in the
184+
// target environment.
185+
// NOTE: This special case seems reasonable for now, but if further divergence occurs
186+
// we should consider doing a more direct solution by having the polyfill definitions
187+
// report names of `FeatureSet` values representing browser `FeatureSet` year instead of
188+
// spec release year.
189+
polyfillSupportFeatureSet =
190+
polyfillSupportFeatureSet.without(
191+
Feature.REGEXP_FLAG_S,
192+
Feature.REGEXP_LOOKBEHIND,
193+
Feature.REGEXP_NAMED_GROUPS,
194+
Feature.REGEXP_UNICODE_PROPERTY_ESCAPE);
195+
return polyfillSupportFeatureSet;
149196
}
150197

151198
@AutoValue

src/com/google/javascript/jscomp/RewritePolyfills.java

+45-25
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package com.google.javascript.jscomp;
1818

19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.common.base.Preconditions.checkState;
21+
import static com.google.common.collect.ImmutableList.toImmutableList;
22+
1923
import com.google.common.annotations.VisibleForTesting;
24+
import com.google.common.collect.ImmutableList;
25+
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
2026
import com.google.javascript.jscomp.PolyfillUsageFinder.Polyfill;
2127
import com.google.javascript.jscomp.PolyfillUsageFinder.PolyfillUsage;
2228
import com.google.javascript.jscomp.PolyfillUsageFinder.Polyfills;
2329
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
24-
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
2530
import com.google.javascript.jscomp.resources.ResourceLoader;
2631
import com.google.javascript.rhino.IR;
2732
import com.google.javascript.rhino.Node;
@@ -53,6 +58,7 @@ public class RewritePolyfills implements CompilerPass {
5358
private final boolean injectPolyfills;
5459
private final boolean isolatePolyfills;
5560
private Set<String> libraries;
61+
private final LanguageMode injectPolyfillsNewerThan;
5662

5763
/**
5864
* @param injectPolyfills if true, injects $jscomp.polyfill initializations into the first input.
@@ -61,25 +67,31 @@ public class RewritePolyfills implements CompilerPass {
6167
* IsolatePolyfills} to prevent their deletion.
6268
*/
6369
public RewritePolyfills(
64-
AbstractCompiler compiler, boolean injectPolyfills, boolean isolatePolyfills) {
70+
AbstractCompiler compiler,
71+
boolean injectPolyfills,
72+
boolean isolatePolyfills,
73+
LanguageMode injectPolyfillsNewerThan) {
6574
this(
6675
compiler,
6776
Polyfills.fromTable(
6877
ResourceLoader.loadTextResource(RewritePolyfills.class, "js/polyfills.txt")),
6978
injectPolyfills,
70-
isolatePolyfills);
79+
isolatePolyfills,
80+
injectPolyfillsNewerThan);
7181
}
7282

7383
@VisibleForTesting
7484
RewritePolyfills(
7585
AbstractCompiler compiler,
7686
Polyfills polyfills,
7787
boolean injectPolyfills,
78-
boolean isolatePolyfills) {
88+
boolean isolatePolyfills,
89+
LanguageMode injectPolyfillsNewerThan) {
7990
this.compiler = compiler;
8091
this.polyfills = polyfills;
8192
this.injectPolyfills = injectPolyfills;
8293
this.isolatePolyfills = isolatePolyfills;
94+
this.injectPolyfillsNewerThan = injectPolyfillsNewerThan;
8395
}
8496

8597
@Override
@@ -96,22 +108,42 @@ public void process(Node externs, Node root) {
96108
compiler.reportChangeToEnclosingScope(jscompLookupMethodDecl);
97109
}
98110

99-
if (!this.injectPolyfills) {
111+
if (!this.injectPolyfills && this.injectPolyfillsNewerThan == null) {
100112
// Nothing left to do. Probably this pass only needed to run because --isolate_polyfills is
101113
// enabled but not --rewrite_polyfills.
102114
return;
103115
}
116+
if (this.injectPolyfills) {
117+
this.libraries = new LinkedHashSet<>();
118+
new PolyfillUsageFinder(compiler, polyfills).traverseExcludingGuarded(root, this::inject);
119+
}
104120

105-
this.libraries = new LinkedHashSet<>();
106-
new PolyfillUsageFinder(compiler, polyfills).traverseExcludingGuarded(root, this::inject);
107-
108-
if (libraries.isEmpty()) {
109-
return;
121+
final ImmutableList<String> librariesToInject;
122+
if (this.injectPolyfillsNewerThan != null) {
123+
ImmutableList<Polyfill> polyfillsToInject =
124+
polyfills.getPolyfillsNewerThan(this.injectPolyfillsNewerThan);
125+
librariesToInject =
126+
polyfillsToInject.stream()
127+
// Skip polyfills that have no associated library. This is true for language
128+
// features like `Proxy` and `String.raw` that have no associated polyfill, hence
129+
// there's
130+
// nothing to inject here.
131+
.filter(p -> !p.library.isEmpty())
132+
.map(p -> p.library)
133+
.collect(toImmutableList());
134+
} else {
135+
librariesToInject = ImmutableList.copyOf(this.libraries);
110136
}
111137

138+
this.injectAll(librariesToInject, /* forceInjection= */ this.injectPolyfillsNewerThan != null);
139+
}
140+
141+
private void injectAll(Iterable<String> librariesToInject, boolean forceInjection) {
112142
Node lastNode = null;
113-
for (String library : libraries) {
114-
lastNode = compiler.ensureLibraryInjected(library, false);
143+
for (String library : librariesToInject) {
144+
checkNotNull(library);
145+
checkState(!library.isEmpty(), "unexpected empty library");
146+
lastNode = compiler.ensureLibraryInjected(library, forceInjection);
115147
}
116148
if (lastNode != null) {
117149
Node parent = lastNode.getParent();
@@ -153,20 +185,8 @@ private void removeUnneededPolyfills(Node parent, Node runtimeEnd) {
153185
if (JSCOMP_POLYFILL.matches(name)) {
154186
final String nativeVersionStr = name.getNext().getNext().getNext().getString();
155187
polyfillSupportFeatureSet = FeatureSet.valueOf(nativeVersionStr);
156-
// Safari has been really slow to implement these regex features, even though it has
157-
// kept on top of the features we polyfill, so we want to ignore the regex features
158-
// when deciding whether the polyfill should be considered "already supported" in the
159-
// target environment.
160-
// NOTE: This special case seems reasonable for now, but if further divergence occurs
161-
// we should consider doing a more direct solution by having the polyfill definitions
162-
// report names of `FeatureSet` values representing browser `FeatureSet` year instead of
163-
// spec release year.
164188
polyfillSupportFeatureSet =
165-
polyfillSupportFeatureSet.without(
166-
Feature.REGEXP_FLAG_S,
167-
Feature.REGEXP_LOOKBEHIND,
168-
Feature.REGEXP_NAMED_GROUPS,
169-
Feature.REGEXP_UNICODE_PROPERTY_ESCAPE);
189+
PolyfillUsageFinder.getPolyfillSupportedFeatureSet(nativeVersionStr);
170190
}
171191
}
172192
return polyfillSupportFeatureSet;

src/com/google/javascript/jscomp/TranspilationPasses.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ static final PassFactory getEs6RewriteDestructuring(ObjectDestructuringRewriteMo
311311
new RewritePolyfills(
312312
compiler,
313313
compiler.getOptions().getRewritePolyfills(),
314-
compiler.getOptions().getIsolatePolyfills()))
314+
compiler.getOptions().getIsolatePolyfills(),
315+
compiler.getOptions().getInjectPolyfillsNewerThan()))
315316
.build();
316317

317318
static final PassFactory instrumentAsyncContext =

test/com/google/javascript/jscomp/RewritePolyfillsTest.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public final class RewritePolyfillsTest extends CompilerTestCase {
4545
private final List<String> polyfillTable = new ArrayList<>();
4646
private boolean isolatePolyfills = false;
4747
private boolean injectPolyfills = true;
48+
private LanguageMode injectPolyfillsNewerThan = null;
4849

4950
private void addLibrary(String name, String from, String to, @Nullable String library) {
5051
if (library != null) {
@@ -61,6 +62,7 @@ public void setUp() throws Exception {
6162
super.setUp();
6263
injectableLibraries.clear();
6364
polyfillTable.clear();
65+
injectPolyfillsNewerThan = null;
6466
setLanguageOut(LanguageMode.ECMASCRIPT5);
6567
}
6668

@@ -70,7 +72,8 @@ protected CompilerPass getProcessor(Compiler compiler) {
7072
compiler,
7173
Polyfills.fromTable(Joiner.on("\n").join(polyfillTable)),
7274
injectPolyfills,
73-
isolatePolyfills);
75+
isolatePolyfills,
76+
injectPolyfillsNewerThan);
7477
}
7578

7679
@Override
@@ -590,4 +593,29 @@ public void testNoCodeChangesIfInjectionDisabled() {
590593
allowExternsChanges();
591594
testSame("'x'.endsWith('y');");
592595
}
596+
597+
@Test
598+
public void testForceInject_es5_addsES6AndES8() {
599+
injectPolyfillsNewerThan = LanguageMode.ECMASCRIPT5;
600+
addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith");
601+
addLibrary("Object.values", "es8", "es3", "es6/object/values");
602+
603+
testInjects("", "es6/string/endswith", "es6/object/values");
604+
}
605+
606+
@Test
607+
public void testForceInject_es2015_addsES8Polyfill() {
608+
injectPolyfillsNewerThan = LanguageMode.ECMASCRIPT5;
609+
addLibrary("Object.values", "es8", "es3", "es6/object/values");
610+
611+
testInjects("", "es6/object/values");
612+
}
613+
614+
@Test
615+
public void testForceInject_es2015_skipsEs2015Polyfills() {
616+
injectPolyfillsNewerThan = LanguageMode.ECMASCRIPT_2015;
617+
addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith");
618+
619+
testDoesNotInject("");
620+
}
593621
}

0 commit comments

Comments
 (0)