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

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

/*
 * 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.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * 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 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(Node conditional, boolean safe, 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(); return target.isName() && PROPERTY_TEST_FUNCTIONS.contains(target.getString()); } // NOTE: we currently assume these are simple (unqualified) names. private static final ImmutableSet PROPERTY_TEST_FUNCTIONS = ImmutableSet.of("String", "Boolean"); // 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