/* * 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 static com.google.common.base.Strings.nullToEmpty; import com.google.common.base.Joiner; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.RewritePolyfills.Polyfills; import com.google.javascript.rhino.Node; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** Unit tests for the RewritePolyfills compiler pass. */ public final class RewritePolyfillsTest extends CompilerTestCase { private static final LanguageMode ES6 = LanguageMode.ECMASCRIPT_2015; private static final LanguageMode ES5 = LanguageMode.ECMASCRIPT5_STRICT; private static final LanguageMode ES3 = LanguageMode.ECMASCRIPT3; private final Map<String, String> injectableLibraries = new HashMap<>(); private final List<String> polyfillTable = new ArrayList<>(); private void addLibrary(String name, String from, String to, String library) { if (library != null) { injectableLibraries.put( library, String.format("$jscomp.polyfill('%s', function() {}, '%s', '%s');\n", name, from, to)); } polyfillTable.add(String.format("%s %s %s %s", name, from, to, nullToEmpty(library))); } @Override public void setUp() throws Exception { super.setUp(); injectableLibraries.clear(); polyfillTable.clear(); } @Override protected CompilerPass getProcessor(Compiler compiler) { return new RewritePolyfills(compiler, Polyfills.fromTable(Joiner.on("\n").join(polyfillTable))); } @Override protected CompilerOptions getOptions() { CompilerOptions options = super.getOptions(); options.setWarningLevel(DiagnosticGroups.MISSING_POLYFILL, CheckLevel.WARNING); return options; } @Override protected Compiler createCompiler() { return new NoninjectingCompiler() { Node lastInjected = null; @Override Node ensureLibraryInjected(String library, boolean force) { Node parent = getNodeForCodeInsertion(null); Node ast = parseSyntheticCode(injectableLibraries.get(library)); Node lastChild = ast.getLastChild(); Node firstChild = ast.removeChildren(); NodeUtil.markNewScopesChanged(firstChild, this); if (lastInjected == null) { parent.addChildrenToFront(firstChild); } else { parent.addChildrenAfter(firstChild, lastInjected); } return lastInjected = lastChild; } }; } @Override protected int getNumRepetitions() { return 1; } private String addLibraries(String code, String[] libraries) { StringBuilder expected = new StringBuilder(); for (String library : libraries) { expected.append(injectableLibraries.get(library)); } expected.append(code); return expected.toString(); } private void testDoesNotInject(String code) { testInjects(code); // empty list of injections } private void testInjects(String code, String... libraries) { test(code, addLibraries(code, libraries)); } private void testInjects(String code, DiagnosticType warning, String... libraries) { test(code, addLibraries(code, libraries), null, warning); } public void testEmpty() { setLanguage(ES6, ES5); testInjects(""); } public void testClassesInjected() { addLibrary("Map", "es6", "es5", "es6/map"); addLibrary("Set", "es6", "es3", "es6/set"); setLanguage(ES6, ES5); testInjects("var m = new Map();", "es6/map"); testInjects("var m = new goog.global.Map();", "es6/map"); testInjects("var Map = goog.global.Map; new Map();", "es6/map"); testInjects("var m = new window.Map();", "es6/map"); setLanguage(ES6, ES3); testInjects("var s = new Set();", "es6/set"); } public void testLibrariesOnlyInjectedOnce() { addLibrary("Map", "es6", "es5", "es6/map"); setLanguage(ES6, ES5); testInjects("var m = new Map(); m = new Map();", "es6/map"); } public void testClassesNotInjectedIfSufficientLanguageOut() { addLibrary("Proxy", "es6", "es6", null); addLibrary("Map", "es6", "es5", "es6/map"); addLibrary("Set", "es6", "es3", "es6/set"); setLanguage(ES6, ES6); testDoesNotInject("new Proxy();"); testDoesNotInject("var m = new Map();"); testDoesNotInject("new Set();"); } public void testClassesNotInjectedIfDeclaredInScope() { addLibrary("Map", "es6", "es5", "es6/map"); setLanguage(ES6, ES5); testDoesNotInject("/** @constructor */ var Map = function() {}; new Map();"); } public void testClassesWarnIfInsufficientLanguageOut() { addLibrary("Proxy", "es6", "es6", null); addLibrary("Map", "es6", "es5", "es6/map"); setLanguage(ES6, ES5); testInjects("new Proxy();", RewritePolyfills.INSUFFICIENT_OUTPUT_VERSION_ERROR); setLanguage(ES6, ES3); testInjects("new Map();", RewritePolyfills.INSUFFICIENT_OUTPUT_VERSION_ERROR, "es6/map"); } public void testStaticMethodsInjected() { addLibrary("Math.clz32", "es6", "es5", "es6/math/clz32"); addLibrary("Array.of", "es6", "es3", "es6/array/of"); addLibrary("Object.keys", "es5", "es3", "es5/object/keys"); setLanguage(ES6, ES5); testInjects("Math.clz32(x);", "es6/math/clz32"); setLanguage(ES6, ES3); testInjects("Array.of(x);", "es6/array/of"); setLanguage(ES6, ES3); testInjects("Object.keys(x);", "es5/object/keys"); } public void testStaticMethodsNotInjectedIfSufficientLanguageOut() { addLibrary("Array.from", "es6", "es6", null); addLibrary("Math.clz32", "es6", "es5", "es6/math/clz32"); addLibrary("Array.of", "es6", "es3", "es6/array/of"); addLibrary("Object.keys", "es5", "es3", "es5/object/keys"); setLanguage(ES6, ES6); testDoesNotInject("Array.from(x);"); testDoesNotInject("Math.clz32(x);"); testDoesNotInject("Array.of(x);"); setLanguage(ES5, ES5); testDoesNotInject("Object.keys(x);"); } public void testStaticMethodsNotInjectedIfDeclaredInScope() { addLibrary("Math.clz32", "es6", "es5", "es6/math/clz32"); setLanguage(ES6, ES5); testDoesNotInject("var Math = {clz32: function() {}}; Math.clz32(x);"); } public void testStaticMethodsWarnIfInsufficientLanguageOut() { addLibrary("Array.from", "es6", "es6", null); addLibrary("Math.clz32", "es6", "es5", "es6/math/clz32"); setLanguage(ES6, ES5); testInjects("Array.from(x);", RewritePolyfills.INSUFFICIENT_OUTPUT_VERSION_ERROR); setLanguage(ES6, ES3); testInjects( "Math.clz32(x);", RewritePolyfills.INSUFFICIENT_OUTPUT_VERSION_ERROR, "es6/math/clz32"); } public void testStaticMethodsNotInstalledIfGuardedByIf() { addLibrary("Array.of", "es6", "es5", "es6/array/of"); testDoesNotInject("if (Array.of) { Array.of(); } else { Array.of(); }"); testDoesNotInject("if (x || Array.of) { Array.of(x); }"); testDoesNotInject("if (x && Array.of) { Array.of(x); }"); testDoesNotInject("if (!Array.of) { Array.of(x); }"); testDoesNotInject("if (Array.of != 'x') { Array.of(x); }"); testDoesNotInject("if (Array.of !== 'x') { Array.of(x); }"); testDoesNotInject("if (Array.of == 'x') { Array.of(x); }"); testDoesNotInject("if (Array.of === 'x') { Array.of(x); }"); testDoesNotInject("if (typeof Array.of == 'function') { Array.of(); }"); } public void testStaticMethodsNotInstalledIfGuardedByLogicalOperator() { addLibrary("Array.of", "es6", "es5", "es6/array/of"); testDoesNotInject("Array.of && Array.of();"); testDoesNotInject("!Array.of || Array.of();"); // NOTE: needs to be first argument to actually guard. testInjects("x && Array.of;", "es6/array/of"); // NOTE: || is not safe by itself. testInjects("Array.of || Array.of();", "es6/array/of"); } public void testStaticMethodsNotInstalledIfGuardedByHook() { addLibrary("Array.of", "es6", "es5", "es6/array/of"); testDoesNotInject("var x = Array.of ? y : function(z) { Array.of(z); };"); testDoesNotInject("var x = Array.of ? function(y) { Array.of(y); } : z;"); testDoesNotInject("typeof Array.of ? Array.of(x) : Array.of(y);"); testDoesNotInject("String(Array.of) == 'foo' ? Array.of(x) : Array.of(y);"); testDoesNotInject("Array.of instanceof Function ? Array.of(x) : Array.of(y);"); // NOTE: needs to be first argument to actually guard. testInjects("x ? (Array.of ? y : z) : Array.of(x)", "es6/array/of"); } public void testStaticMethodsNotInstalledIfGuardedByAbruptReturn() { addLibrary("Array.of", "es6", "es5", "es6/array/of"); testDoesNotInject("if (!Array.of) throw 'x'; Array.of();"); testDoesNotInject("!function() { if (!Array.of) return; Array.of(); }"); testDoesNotInject("if (!Array.of) { throw 'x'; } Array.of();"); // NOTE: abrupt return must be a sibling conditional's direct child testInjects("{ if (!Array.of) throw 'x'; } Array.of();", "es6/array/of"); testInjects("!function() { { if (!Array.of) return; } Array.of(); }()", "es6/array/of"); testInjects("if (!Array.of) { { throw 'x'; } } Array.of();", "es6/array/of"); testInjects("{ if (!Array.of) throw 'x'; throw 'x'; } Array.of();", "es6/array/of"); testInjects("{ if (Array.of) throw 'x'; { throw 'x'; } } Array.of();", "es6/array/of"); testInjects( "if (unrelated) { if (Array.of) throw 'x'; { throw 'x'; } } else Array.of();", "es6/array/of"); } public void testPrototypeMethodsInjected() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); addLibrary("Array.prototype.fill", "es6", "es3", "es6/array/fill"); addLibrary("Array.prototype.forEach", "es5", "es3", "es5/array/foreach"); setLanguage(ES6, ES5); testInjects("x.endsWith(y);", "es6/string/endswith"); testInjects("x.fill();", "es6/array/fill"); setLanguage(ES5, ES3); testInjects("x.forEach(y);", "es5/array/foreach"); } public void testPrototypeMethodsNotInjectedIfSufficientLanguageOut() { addLibrary("String.prototype.normalize", "es6", "es6", null); addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); addLibrary("Array.prototype.fill", "es6", "es3", "es6/array/fill"); addLibrary("Array.prototype.forEach", "es5", "es3", "es5/array/foreach"); setLanguage(ES6, ES6); testDoesNotInject("x.normalize();"); testDoesNotInject("x.endsWith();"); testDoesNotInject("x.fill(y);"); setLanguage(ES5, ES5); testDoesNotInject("x.forEach();"); } public void testMultiplePrototypeMethodsWithSameName() { addLibrary("Array.prototype.includes", "es6", "es3", "es6/array/includes"); addLibrary("String.prototype.includes", "es5", "es3", "es5/string/includes"); setLanguage(ES6, ES5); testInjects("x.includes();", "es6/array/includes"); setLanguage(ES5, ES3); testInjects("x.includes();", "es6/array/includes", "es5/string/includes"); } public void testPrototypeMethodsInstalledIfStaticMethodShadowed() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); setLanguage(ES6, ES5); testInjects( "var string = {}; string.endsWith = function() {}; " + "string.foo = function(string) { return string.endsWith('x'); };", "es6/string/endswith"); } // NOTE(sdh): it's not clear what makes the most sense here. At one point we // took care to avoid installing these, but it may make sense to instead leave // this distinction to a type-based optimization. As such, I've simplified the // logic to no longer look at variables' scope and instead just blacklist known // symbols like goog.string and goog.array. public void testPrototypeMethodsInstalledIfActuallyStatic() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); setLanguage(ES6, ES5); testInjects( "var string = {}; string.endsWith = function() {}; string.endsWith('x');", "es6/string/endswith"); testInjects( "var string = {endsWith: function() {}}; string.endsWith('x');", "es6/string/endswith"); testInjects( "var string = {}; string.endsWith = function() {}; " + "string.foo = function() { return string.endsWith('x'); };", "es6/string/endswith"); } public void testPrototypeMethodsNotInstalledIfGuardedByIf() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); testDoesNotInject("if (String.prototype.endsWith) { x.endsWith(); } else { y.endsWith(); }"); testDoesNotInject("if (x || String.prototype.endsWith) { x.endsWith(); }"); testDoesNotInject("if (x && String.prototype.endsWith) { x.endsWith(); }"); testDoesNotInject("if (!String.prototype.endsWith) { x.endsWith(); }"); testDoesNotInject("if (String.prototype.endsWith != 'x') { x.endsWith(); }"); testDoesNotInject("if (String.prototype.endsWith !== 'x') { x.endsWith(); }"); testDoesNotInject("if (String.prototype.endsWith == 'x') { x.endsWith(); }"); testDoesNotInject("if (String.prototype.endsWith === 'x') { x.endsWith(); }"); testDoesNotInject("if (typeof String.prototype.endsWith == 'function') { x.endsWith(); }"); } public void testPrototypeMethodsNotInstalledIfGuardedByLogicalOperator() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); testDoesNotInject("String.prototype.endsWith && x.endsWith();"); testDoesNotInject("!String.prototype.endsWith || x.endsWith();"); testDoesNotInject("x.endsWith && x.endsWith();"); // NOTE: needs to be first argument to actually guard. testInjects("x && x.endsWith;", "es6/string/endswith"); // NOTE: || is not safe by itself. testInjects("String.prototype.endsWith || x.endsWith();", "es6/string/endswith"); } public void testPrototypeMethodsNotInstalledIfGuardedByHook() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); testDoesNotInject("var x = String.prototype.endsWith ? y : function(z) { z.endsWith(); };"); testDoesNotInject("var x = String.prototype.endsWith ? function(y) { y.endsWith(); } : z;"); testDoesNotInject("typeof String.prototype.endsWith ? x.endsWith() : y.endsWith();"); testDoesNotInject("String(x.endsWith) == 'foo' ? x.endsWith() : y.endsWith();"); testDoesNotInject("Boolean(x.endsWith) ? x.endsWith() : y.endsWith();"); // NOTE: needs to be first argument to actually guard. testInjects("x ? (String.prototype.endsWith ? y : z) : x.endsWith()", "es6/string/endswith"); } public void testPrototypeMethodsNotInstalledIfGuardedByAbruptReturn() { addLibrary("String.prototype.endsWith", "es6", "es5", "es6/string/endswith"); testDoesNotInject("if (!x.endsWith) throw 'x'; y.endsWith();"); testDoesNotInject("if (!x.endsWith) { throw 'x'; } y.endsWith();"); testDoesNotInject("!function() { if (!x.endsWith) return; y.endsWith(); }()"); // NOTE: abrupt return must be a sibling conditional's direct child testInjects("{ if (!x.endsWith) throw 'x'; } y.endsWith();", "es6/string/endswith"); testInjects("if (!x.endsWith) { { throw 'x'; } } y.endsWith();", "es6/string/endswith"); testInjects( "!function() { { if (!x.endsWith) return; } y.endsWith(); }()", "es6/string/endswith"); testInjects("{ if (!x.endsWith) throw 'x'; throw 'x'; } y.endsWith();", "es6/string/endswith"); testInjects( "{ if (x.endsWith) throw 'x'; { throw 'x'; } } x.endsWith();", "es6/string/endswith"); testInjects( "if (unrelated) { if (x.endsWith) throw 'x'; { throw 'x'; } } else x.endsWith();", "es6/string/endswith"); } public void testCleansUpUnnecessaryPolyfills() { // Put two polyfill statements in the same library. injectableLibraries.put("es6/set", "$jscomp.polyfill('Set', '', 'es6', 'es3'); $jscomp.polyfill('Map', '', 'es5', 'es3');"); polyfillTable.add("Set es6 es3 es6/set"); setLanguage(ES6, ES5); test("var set = new Set();", "$jscomp.polyfill('Set', '', 'es6', 'es3'); var set = new Set();"); setLanguage(ES6, ES3); test( "var set = new Set();", "$jscomp.polyfill('Set', '', 'es6', 'es3'); $jscomp.polyfill('Map', '', 'es5', 'es3');" + "var set = new Set();"); } }