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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * 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.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.CompilerOptions.PropertyCollapseLevel;
import com.google.javascript.jscomp.PolyfillUsageFinder.Polyfill;
import com.google.javascript.jscomp.PolyfillUsageFinder.PolyfillUsage;
import com.google.javascript.jscomp.PolyfillUsageFinder.Polyfills;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.resources.ResourceLoader;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

/**
 * Rewrites potential polyfill usages to use the hidden JSCompiler polyfills instead of the global.
 *
 * 

When $jscomp.ISOLATE_POLYFILLS is enabled, the $jscomp.polyfill library function does not add * polyfills to the global scope or as properties on the native type. Instead, classes like Map are * added to the $jscomp.polyfills object. Methods like `String.prototype.includes` are defined under * a unique Symbol on String.prototype. * *

This pass rewrites polyfill usages so that they access the actual polyfills instead of trying * to access the native types. For example, new Map() becomes * new $jscomp.polyfills['Map']. * *

Limitations of this pass: a) it ignores destructuring and b) it does not support rewriting * writes to polyfilled methods. */ class IsolatePolyfills implements CompilerPass { private final AbstractCompiler compiler; private final Polyfills polyfills; private final Node jscompPolyfillsObject; private static final String POLYFILL_TEMP = "$jscomp$polyfillTmp"; private final Node jscompLookupMethod = IR.name("$jscomp$lookupPolyfilledValue"); private boolean usedPolyfillMethodLookup = false; private boolean isTempVarInitialized = false; IsolatePolyfills(AbstractCompiler compiler) { this( compiler, Polyfills.fromTable( ResourceLoader.loadTextResource(IsolatePolyfills.class, "js/polyfills.txt"))); } @VisibleForTesting IsolatePolyfills(AbstractCompiler compiler, Polyfills polyfills) { this.compiler = compiler; this.polyfills = polyfills; boolean hasPropertyCollapsingRun = compiler.getOptions().getPropertyCollapseLevel().equals(PropertyCollapseLevel.ALL); jscompPolyfillsObject = hasPropertyCollapsingRun ? createCollapsedName() : createJSCompPolyfillsAccess(); } /** Returns a name `$jscomp$polyfills` */ private static Node createCollapsedName() { Node collapsedName = IR.name("$jscomp$polyfills"); collapsedName.putBooleanProp(Node.IS_CONSTANT_NAME, true); return collapsedName; } /** Returns a getprop `$jscomp.polyfills` */ private static Node createJSCompPolyfillsAccess() { Node jscomp = IR.name("$jscomp"); jscomp.putBooleanProp(Node.IS_CONSTANT_NAME, true); return IR.getprop(jscomp, "polyfills"); } @Override public void process(Node externs, Node root) { // Calculate the set of polyfills that are actually present in the AST. It may be a subset of // the potential polyfills which PolyfillFindingCallback finds (it's fine if it's a superset.) ImmutableSet injectedPolyfills = findAllInjectedPolyfills(); List polyfillUsages = new ArrayList<>(); new PolyfillUsageFinder(compiler, this.polyfills) .traverseIncludingGuarded(root, polyfillUsages::add); LinkedHashSet visitedNodes = new LinkedHashSet<>(); for (PolyfillUsage usage : polyfillUsages) { if ( // Some nodes map to more than one polyfill usage. For example, `x.includes` maps to both // Array.prototype.includes and String.prototype.includes, but only needs to be isolated once. visitedNodes.contains(usage.node()) // Skip visiting nodes whose 'polyfill.library' is empty. This is true for language // features like `Proxy` and `String.raw` that have no associated polyfill, and hence are // unnecessary to isolate. || usage.polyfill().library.isEmpty() // The PolyfillFindingCallback may detect possible polyfill usages that are not // in fact injected. (possibly because RemoveUnusedCode deleted the polyfill.) || !injectedPolyfills.contains(usage.polyfill().nativeSymbol)) { continue; } this.rewritePolyfill(usage); visitedNodes.add(usage.node()); } cleanUpJscompLookupPolyfilledValue(); } /** * Searches the AST for all calls to $jscomp.polyfill and returns the polyfilled symbol names. * *

At the moment we don't track anywhere the set of all polyfills that have been injected. That * set may be modified by RewritePolyfills, Es6InjectRuntimeLibraries, and RemoveUnusedCode. If * desired, we could delete this method by making any passes that add/remove polyfill calls * responsible for tracking their presence. * *

Note: this set cannot be injected into the constructor because it is not known until the * polyfill injection pass actually runs. */ private ImmutableSet findAllInjectedPolyfills() { ImmutableSet.Builder actualPolyfills = ImmutableSet.builder(); Node lastInjectedNode = compiler.getNodeForCodeInsertion(null); NodeTraversal.traverse( compiler, lastInjectedNode, new NodeTraversal.AbstractShallowCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (isJSCompPolyfillCall(n)) { // CALL // GETPROP/NAME $jscomp.polyfill // STRING NativeSymbol.prototype.method // [...] String polyfilledSymbol = n.getSecondChild().getString(); actualPolyfills.add(polyfilledSymbol); } } }); return actualPolyfills.build(); } private boolean isJSCompPolyfillCall(Node call) { if (!call.isCall()) { return false; } String jscompPolyfillName = compiler.getOptions().getPropertyCollapseLevel().equals(PropertyCollapseLevel.ALL) ? "$jscomp$polyfill" : "$jscomp.polyfill"; return call.getFirstChild().matchesQualifiedName(jscompPolyfillName); } /** * Rewrites a potential access of a polyfilled class or method to first look for the non-global, * polyfilled version. */ private void rewritePolyfill(PolyfillUsage polyfillUsage) { final Polyfill polyfill = polyfillUsage.polyfill(); // If the output FeatureSet includes all of the features of the language version in which // the polyfilled symbol was introduced, we can assume that the intended platform has // the symbol defined natively, so the compiler won't include the polyfill in its output and it // won't need to be isolated. if (compiler .getOptions() .getOutputFeatureSet() .contains(FeatureSet.valueOf(polyfill.nativeVersion))) { return; } final Node polyfillAccess = polyfillUsage.node(); final Node parent = polyfillUsage.node().getParent(); if (FILES_ALLOWED_UNQUALIFIED_POLYFILL_ACCESSES.contains(polyfillAccess.getSourceFileName())) { // Special-case code in the compiler runtime libraries that executes before any polyfills are // injected. // - the definition of $jscomp.global needs to look for a global variable globalThis // - the $jscomp.polyfill function needs to check whether Symbol is native // If desired we could programatically detect these cases instead of having this allowlist of // polyfill accesses, but that might silently allow other usages of third-party polyfills // into the codebase. // TODO(b/156776817): crash on early references to polyfills that are not in our allowlist. return; } // For now we need to assume that these lvalues are unrelated to the polyfills, as we are not // using any type information. If we wanted, we could support assignments to properties that are // already present, e.g. // Array.includes = intercept; // to // tmp = Array; // tmp[$maybePolyfillProp(tmp, 'includes)] = intercept; if (NodeUtil.isLValue(polyfillAccess) || (parent.isAssign() && polyfillAccess.isFirstChildOf(parent))) { return; } final String name = polyfillUsage.name(); boolean isGlobalClass = name.indexOf('.') == -1 && polyfill.kind.equals(Polyfill.Kind.STATIC); if (isGlobalClass) { // e.g. Symbol, Map, window.Map, or goog.global.Map polyfillAccess.replaceWith( IR.getelem(jscompPolyfillsObject.cloneTree(), IR.string(name)) // $jscomp.polyfills['Map'] .srcrefTree(polyfillAccess)); } else if (parent.isCall() && polyfillAccess.isFirstChildOf(parent)) { // e.g. getStr().includes('x') rewritePolyfillInCall(polyfillAccess); } else { // e.g. [].includes.call(myIter, 0) Node methodName = IR.string(polyfillAccess.getString()).srcref(polyfillAccess); Node receiver = polyfillAccess.removeFirstChild(); polyfillAccess.replaceWith( createPolyfillMethodLookup(receiver, methodName).srcrefTree(polyfillAccess)); } compiler.reportChangeToEnclosingScope(parent); } private static final ImmutableSet FILES_ALLOWED_UNQUALIFIED_POLYFILL_ACCESSES = ImmutableSet.of( AbstractCompiler.RUNTIME_LIB_DIR + "util/global.js", AbstractCompiler.RUNTIME_LIB_DIR + "util/shouldpolyfill.js"); /** * Rewrites a call where the receiver is a potential polyfilled method * *

Before: receiver.method(arg) * *

After: lookupPolyfilledValue(receiver, 'method').call(receiver, arg) * *

Or, if evaluating the receiver may have side effects, we store the receiver in a temporary * variable to avoid evaluating it twice: * *

After: (tmpNode = receiver, lookupPolyfilledValue(tmpNode, 'method')) * .call(tmpNode, arg) */ private void rewritePolyfillInCall(Node callee) { final Node methodName = IR.string(callee.getString()).srcref(callee); final Node receiver = callee.removeFirstChild(); boolean requiresTemp = compiler.getAstAnalyzer().mayEffectMutableState(receiver); final Node polyfilledMethod; final Node thisNode; if (requiresTemp) { // e.g. `sideEffects().includes(arg)` thisNode = createTempName(callee); // (tmpNode = sideEffects(), lookupMethod(tmpNode, 'includes')) polyfilledMethod = IR.comma( IR.assign(thisNode.cloneTree(), receiver), createPolyfillMethodLookup(thisNode.cloneTree(), methodName)); } else { thisNode = receiver; polyfilledMethod = createPolyfillMethodLookup(receiver.cloneTree(), methodName); } // Fix the `this` type by using .call: // lookupMethod(receiver, 'includes').call(receiver, arg) Node receiverDotCall = IR.getprop(polyfilledMethod, "call").srcrefTree(callee); callee.replaceWith(receiverDotCall); thisNode.insertAfter(receiverDotCall); } private Node createTempName(Node srcref) { if (!isTempVarInitialized) { isTempVarInitialized = true; Node decl = IR.var(IR.name(POLYFILL_TEMP)).srcrefTree(srcref); compiler.getNodeForCodeInsertion(null).addChildToFront(decl); } // The same temporary variable is always used for every polyfill invocation. This is believed // to be safe and makes the code easier to generate and smaller. There's a change it will make // it harder for V8 to optimize, though. If proves to be a problem we could introduce unique tmp // variables. return IR.name(POLYFILL_TEMP).srcref(srcref); } /** Returns a call $jscomp$lookupPolyfilledValue(receiver, 'methodName') */ private Node createPolyfillMethodLookup(Node receiver, Node methodName) { usedPolyfillMethodLookup = true; Node call = IR.call(jscompLookupMethod.cloneTree(), receiver, methodName); call.putBooleanProp(Node.FREE_CALL, true); return call; } /** * Deletes the dummy externs declaration of $jscomp$lookupPolyfilledValue and the function itself * if unused. * *

The RewritePolyfills pass injected a definition of $jscomp$lookupPolyfilledValue into the * externs. This prevented dead code elimination, since the function is never unused until this * pass runs. However, now we need to delete the externs definition so that variable renaming can * actually rename $jscomp$lookupPolyfilledValue. */ private void cleanUpJscompLookupPolyfilledValue() { Node syntheticExternsRoot = compiler.getSynthesizedExternsInput().getAstRoot(compiler); Scope syntheticExternsScope = new SyntacticScopeCreator(compiler).createScope(syntheticExternsRoot, /* parent= */ null); Var externVar = checkNotNull( syntheticExternsScope.getVar(jscompLookupMethod.getString()), "Failed to find synthetic $jscomp$lookupPolyfilledValue extern"); NodeUtil.deleteNode(externVar.getParentNode(), compiler); if (usedPolyfillMethodLookup) { return; } Scope syntheticCodeScope = new SyntacticScopeCreator(compiler) .createScope(compiler.getNodeForCodeInsertion(/* module= */ null), /* parent= */ null); Var syntheticVar = syntheticCodeScope.getVar(jscompLookupMethod.getString()); // Don't error if we can't find a definition for jscompLookupMethod. It's possible that we are // running in transpileOnly mode and are not injecting runtime libraries. if (syntheticVar != null) { NodeUtil.deleteNode(syntheticVar.getParentNode(), compiler); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy