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

com.google.javascript.jscomp.GuardedCallback 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 2016 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.collect.HashMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multiset;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.Deque;
import org.jspecify.nullness.Nullable;

/**
 * An AST traverser that keeps track of whether access to a generic resource are "guarded" or not. A
 * guarded use is one that will not cause runtime errors if the resource does not exist. The
 * resource (type {@code T} in the signature) is any value type computable from a node, such as a
 * qualified name or property name. It is up to the subclass to map the currently visited token to
 * the {@code T} in order to call {@link #isGuarded}, which depends entirely on the context from
 * ancestor nodes and previous calls to {@code #isGuarded} for the same resource.
 *
 * 

More precisely, a resource may be guarded either intrinsically or conditionally, * as follows. A use is intrinsically guarded if it occurs in a context where its specific value is * immediately discarded, such as coercion to boolean or string. A use is conditionally guarded if * it occurs in a context conditioned on an intrinsically guarded use (such as the "then" or "else" * block of an "if", the second or third argument of a ternary operator, right-hand arguments of * logical operators, or later in a block in which an unconditional "throw" or "return" was found in * a guarded context). * *

For example, the following are all intrinsically guarded uses of {@code x}: * *

{@code
 * // Coerced to boolean:
 * if (x);
 * x ? y : z;
 * x && y;
 * Boolean(x);
 * !x;
 * x == y; x != y; x === y; x !== y;
 * x instanceof Foo;
 * typeof x === 'string';
 *
 * // Coerced to string:
 * String(x);
 * typeof x;
 *
 * // Immediately discarded (but doesn't make much sense in my contexts):
 * x = y;
 * }
* *

The following uses of {@code x} are conditionally guarded: * *

{@code
 * if (x) x();
 * !x ? null : x;
 * typeof x == 'function' ? x : () => {};
 * x && x.y;
 * !x || x.y;
 * if (!x) return; x();
 * x ?? x = 3;
 * }
* * Note that there is no logic to determine which branch is guarded, so any usages in either * branch will pass after such a check. As such, the following are also considered guarded, though a * human can easily see that this is spurious: * *
{@code
 * if (!x) x();
 * if (x) { } else x();
 * !x && x();
 * var y = x != null ? null : x;
 * }
* * Note also that the call or property access is not necessary to make a use unguarded: the final * example immediately above would be unguarded if it weren't for the {@code x != null} condition, * since it allows the value of {@code x} to leak out in an uncontrolled way. * *

This class overrides the {@link Callback} API methods with final methods of its own, and * defines the template method {@link #visitGuarded} to perform the normal work for individual * nodes. The only other API is {@link #isGuarded}, which allows checking if a {@code T} in the * current node's context is guarded, either intrinsically or conditionally. If it is intrinsically * guarded, then it may be recorded as a condition for the purpose of guarding future contexts. */ abstract class GuardedCallback implements NodeTraversal.Callback { // Compiler is needed for coding convention (isPropertyTestFunction). private final AbstractCompiler compiler; // Map from short-circuiting conditional nodes (AND, OR, COALESCE, IF, and HOOK) to // the set of resources each node guards. This is saved separately from // just `guarded` because the guard must not go into effect until after // traversal of the first child is complete. Before traversing the second // child any node, its values in this map are moved into `guarded` and // `installedGuards` (the latter allowing removal at the correct time). private final SetMultimap registeredGuards = MultimapBuilder.hashKeys().hashSetValues().build(); // Set of currently-guarded resources. Elements are added to this set // just before traversing the second or later (i.e. "then" or "else") // child of a short-circuiting conditional node, and then removed after // traversing the last child. It is a multiset so that multiple adds // of the same resource require the same number of removals before the // resource becomes unguarded. private final Multiset guarded = HashMultiset.create(); // Resources that are currently installed as guarded but will need to // be removed from `guarded` after visiting all the key nodes' children. private final ListMultimap installedGuards = MultimapBuilder.hashKeys().arrayListValues().build(); // A stack of `Context` objects describing the current node's context: // specifically, whether it is inherently safe, and a link to one or // more conditional nodes in the current statement directly above it // (for registering safe resources as guards). private final Deque contextStack = new ArrayDeque<>(); GuardedCallback(AbstractCompiler compiler) { this.compiler = compiler; } @Override public final boolean shouldTraverse(NodeTraversal traversal, Node n, Node parent) { // Note that shouldTraverse() operates primarily on `parent`, while visit() // uses `n`. This is intentional. To see why, consider traversing the // following tree: // // if (x) y; else z; // // 1. shouldTraverse(`if`): // a. parent is null, so pushes an EMPTY onto the context stack. // 2. shouldTraverse(`x`): // a. parent is `if`, so pushes Context(`if`, true); guards is empty. // 3. visit(`x`) // a. guarded and installedGuards are both empty, so nothing is removed. // b. visitGuarded(`x`) will call isGuarded("x"), which looks at the top // of the stack and sees that the context is safe (true) and that // there is a linked conditional node (the `if`); adds {`if`: "x"} // to registeredGuards. // b. Context(`if`, true) is popped off the stack. // 4. shouldTraverse(`y`): // a. parent is still `if`, but since `y` is the second child it is // no longer safe, so another EMPTY is pushed. // b. the {`if`: "x"} guard is moved from registered to installed. // 5. visit(`y`): // a. nothing is installed on `y` so no guards are removed. // b. visitGuarded(`y`) will call isGuarded("y"), which will return // false since "y" is neither intrinsically or conditionally guarded; // if we'd called isResourceRequired("x"), it would return false // because "x" is currently an element of guarded. // c. one empty context is popped. // 6. shouldTraverse(`z`), visit(`z`) // a. see steps 4-5, nothing really changes here. // 7. visit(`if`) // a. the installed {`if`: "x"} guard is removed. // c. pop the final empty context from the stack. if (parent == null) { // The first node gets an empty context. contextStack.push(Context.EMPTY); } else { // Before traversing any children, we update the stack contextStack.push(contextStack.peek().descend(compiler, parent, n)); // If the parent has any guards registered on it, then add them to both // `guarded` and `installedGuards`. if (parent != null && CAN_HAVE_GUARDS.contains(parent.getToken()) && registeredGuards.containsKey(parent)) { for (T resource : registeredGuards.removeAll(parent)) { guarded.add(resource); installedGuards.put(parent, resource); } } } return true; } @Override public final void visit(NodeTraversal traversal, Node n, Node parent) { // Remove any guards registered on this node by its children, which are no longer // relevant. This happens first because these were registered on a "parent", but // now this is that parent (i.e. `n` here vs `parent` in isGuarded). if (parent != null && CAN_HAVE_GUARDS.contains(n.getToken()) && installedGuards.containsKey(n)) { guarded.removeAll(installedGuards.removeAll(n)); } // Check for abrupt returns (`return` and `throw`). if (isAbrupt(n)) { // If found, any guards installed on a parent IF should be promoted to the // grandparent. This allows a small amount of flow-sensitivity, in that // if (!x) return; x(); // has the guard for `x` promoted from the `if` to the outer block, so that // it guards the next statement. promoteAbruptReturns(parent); } // Finally continue on to whatever the traversal would normally do. visitGuarded(traversal, n, parent); // After children have been traversed, pop the top of the conditional stack. contextStack.pop(); } private void promoteAbruptReturns(Node parent) { // If the parent is a BLOCK (e.g. `if (x) { return; }`) then go up one level. if (parent.isBlock()) { parent = parent.getParent(); } // If there were any guards registered the parent IF, then promote them up one level. if (parent.isIf() && installedGuards.containsKey(parent)) { Node grandparent = parent.getParent(); if (grandparent.isBlock() || grandparent.isScript()) { registeredGuards.putAll(grandparent, installedGuards.get(parent)); } } } /** * Performs specific traversal behavior. Should call {@link #isGuarded} * at least once. */ abstract void visitGuarded(NodeTraversal traversal, Node n, Node parent); /** * Determines if the given resource is guarded, either intrinsically or * conditionally. If the former, any ancestor conditional nodes are * registered as feature-testing the resource. */ boolean isGuarded(T resource) { // Check if this polyfill is already guarded. If so, return true right away. if (guarded.contains(resource)) { return true; } // If not, see if this is itself a feature check guard. This is // defined as a usage of the polyfill in such a way that throws // away the actual value and only cares about its truthiness or // typeof. We walk up the ancestor tree through a small set of // node types and if this is detected to be a guard, then the // conditional node is marked as a guard for this polyfill. Context context = contextStack.peek(); if (!context.safe) { return false; } // Loop over all the linked conditionals and register this as a guard. while (context != null && context.conditional != null) { registeredGuards.put(context.conditional, resource); context = context.linked; } return true; } // The context of a node, keeping track of whether it is safe for // possibly-undefined values, and whether there are any conditionals // upstream in the tree. private static class Context { // An empty instance: unsafe and with no linked conditional nodes. static final Context EMPTY = new Context(null, false, null); // The most recent conditional. final Node conditional; // Whether this position is safe for an undefined type. final boolean safe; // A very naive linked list for storing additional conditional nodes. final Context linked; Context(@Nullable Node conditional, boolean safe, @Nullable Context linked) { this.conditional = conditional; this.safe = safe; this.linked = linked; } // Returns a new Context with a new conditional node and safety status. // If the current context already has a conditional, then it is linked // so that both can be marked when necessary. Context link(Node newConditional, boolean newSafe) { return new Context(newConditional, newSafe, this.conditional != null ? this : null); } // Returns a new Context with a different safety bit, but doesn't // change anything else. Context propagate(boolean newSafe) { return newSafe == safe ? this : new Context(conditional, newSafe, linked); } // Returns a new context given the current context and the next parent // node. Child is only used to determine whether we're looking at the // first child or not. Context descend(AbstractCompiler compiler, Node parent, Node child) { boolean first = child == parent.getFirstChild(); switch (parent.getToken()) { case CAST: // Casts are irrelevant. return this; case COMMA: // `Promise, whatever` is safe. // `whatever, Promise` is same as outer context. return child == parent.getLastChild() ? this : propagate(true); case AND: // `Promise && whatever` never returns Promise itself, so it is safe. // `whatever && Promise` may return Promise, so return outer context. return first ? link(parent, true) : this; case OR: case COALESCE: // `Promise || whatever` and `Promise ?? whatever` // may return Promise (unsafe), but is itself a conditional. // `whatever || Promise` and `whatever ?? Promise` // is same as outer context. return first ? link(parent, false) : this; case HOOK: // `Promise ? whatever : whatever` is a safe conditional. // `whatever ? Promise : whatever` (etc) is same as outer context. return first ? link(parent, true) : this; case IF: // `if (Promise) whatever` is a safe conditional. // `if (whatever) { ... }` is nothing. // TODO(sdh): Handle do/while/for/for-of/for-in? return first ? link(parent, true) : EMPTY; case INSTANCEOF: case ASSIGN: // `Promise instanceof whatever` is safe, `whatever instanceof Promise` is not. // `Promise = whatever` is a bad idea, but it's safe w.r.t. polyfills. return propagate(first); case TYPEOF: case NOT: case EQ: case NE: case SHEQ: case SHNE: // `typeof Promise` is always safe, as is `Promise == whatever`, etc. return propagate(true); case CALL: // `String(Promise)` is safe, `Promise(whatever)` or `whatever(Promise)` is not. return propagate(!first && isPropertyTestFunction(compiler, parent)); case ROOT: // This case causes problems for isStatement() so handle it separately. return EMPTY; case OPTCHAIN_CALL: case OPTCHAIN_GETELEM: case OPTCHAIN_GETPROP: if (first) { // thisNode?.rest.of.chain // OR firstChild?.thisNode.rest.of.chain // For the first case `thisNode` should be considered intrinsically guarded. return link(parent, parent.isOptionalChainStart()); } else { // `first?.(thisNode)` // or `first?.[thisNode]` // or `first?.thisNode` return propagate(false); } default: // Expressions propagate linked conditionals; statements do not. return NodeUtil.isStatement(parent) ? EMPTY : propagate(false); } } } private static boolean isAbrupt(Node n) { return n.isReturn() || n.isThrow(); } // Extend the coding convention's idea of property test functions to also // include String() and Boolean(). private static boolean isPropertyTestFunction(AbstractCompiler compiler, Node n) { if (compiler.getCodingConvention().isPropertyTestFunction(n)) { return true; } Node target = n.getFirstChild(); if (target.isName()) { String name = target.getString(); return name.equals("String") || name.equals("Boolean"); } return false; } // Tokens that are allowed to have guards on them (no point doing a hash lookup on // any other type of node). private static final ImmutableSet CAN_HAVE_GUARDS = Sets.immutableEnumSet( Token.AND, Token.OR, Token.COALESCE, Token.HOOK, Token.IF, Token.BLOCK, Token.SCRIPT, Token.OPTCHAIN_CALL, Token.OPTCHAIN_GETELEM, Token.OPTCHAIN_GETPROP); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy