com.google.javascript.jscomp.ControlFlowAnalysis Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler Show documentation
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.
/*
* Copyright 2008 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Comparator.comparingInt;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
/**
* This is a compiler pass that computes a control flow graph. Note that this is only a CompilerPass
* because the Compiler invokes it via Compiler#process. It is never included in a PassConfig.
*/
public final class ControlFlowAnalysis implements Callback, CompilerPass {
/**
* Based roughly on the first few pages of
*
* "Declarative Intraprocedural Flow Analysis of Java Source Code by
* Nilsson-Nyman, Hedin, Magnusson & Ekman",
*
* this pass computes the control flow graph from the AST. However, a full
* attribute grammar is not necessary. We will compute the flow edges with a
* single post order traversal. The "follow()" of a given node will be
* computed recursively in a demand driven fashion.
*
* As of this moment, we are not performing any inter-procedural analysis
* within our framework.
*/
private final AbstractCompiler compiler;
private ControlFlowGraph cfg;
private Map astPosition;
// TODO(nicksantos): should these be node annotations?
private Map, Integer> nodePriorities;
// We order CFG nodes by by looking at the AST positions.
// CFG nodes that come first lexically should be visited first, because
// they will often be executed first in the source program.
private final Comparator> priorityComparator =
comparingInt(digraphNode -> astPosition.get(digraphNode.getValue()));
private int astPositionCounter;
private int priorityCounter;
private final boolean shouldTraverseFunctions;
private final boolean edgeAnnotations;
// We need to store where we started, in case we aren't doing a flow analysis
// for the whole scope. This happens, for example, when running type inference
// on only the externs.
private Node root;
/*
* This stack captures the structure of nested TRY blocks. The top of the
* stack is the inner most TRY block. A FUNCTION node in this stack implies
* that the handler is determined by the caller of the function at runtime.
*/
private final Deque exceptionHandler = new ArrayDeque<>();
/*
* This map is used to handle the follow of FINALLY. For example:
*
* while(x) {
* try {
* try {
* break;
* } catch (a) {
* } finally {
* foo();
* }
* fooFollow();
* } catch (b) {
* } finally {
* bar();
* }
* barFollow();
* }
* END();
*
* In this case finallyMap will contain a map from:
* first FINALLY -> bar()
* second FINALLY -> END()
*
* When we are connecting foo() and bar() to to their respective follow, we
* must also look up this map and connect:
* foo() -> bar()
* bar() -> END
*/
private final Multimap finallyMap = HashMultimap.create();
/**
* Constructor.
*
* @param compiler Compiler instance.
* @param shouldTraverseFunctions Whether functions should be traversed
* @param edgeAnnotations Whether to allow edge annotations.
*/
ControlFlowAnalysis(
AbstractCompiler compiler, boolean shouldTraverseFunctions, boolean edgeAnnotations) {
this.compiler = compiler;
this.shouldTraverseFunctions = shouldTraverseFunctions;
this.edgeAnnotations = edgeAnnotations;
}
public static ControlFlowGraph getCfg(AbstractCompiler compiler, Node cfgRoot) {
checkArgument(NodeUtil.isValidCfgRoot(cfgRoot));
ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, true);
cfa.process(null, cfgRoot);
return cfa.getCfg();
}
ControlFlowGraph getCfg() {
return cfg;
}
@Override
public void process(Node externs, Node root) {
Preconditions.checkArgument(
NodeUtil.isValidCfgRoot(root), "Unexpected control flow graph root %s", root);
this.root = root;
astPositionCounter = 0;
astPosition = new HashMap<>();
nodePriorities = new HashMap<>();
cfg = new AstControlFlowGraph(computeFallThrough(root), nodePriorities, edgeAnnotations);
NodeTraversal.traverse(compiler, root, this);
astPosition.put(null, ++astPositionCounter); // the implicit return is last.
// Now, generate the priority of nodes by doing a depth-first
// search on the CFG.
priorityCounter = 0;
DiGraphNode entry = cfg.getEntry();
prioritizeFromEntryNode(entry);
if (shouldTraverseFunctions) {
// If we're traversing inner functions, we need to rank the
// priority of them too.
for (DiGraphNode candidate : cfg.getNodes()) {
Node value = candidate.getValue();
if (value != null && value.isFunction()) {
prioritizeFromEntryNode(candidate);
}
}
}
// At this point, all reachable nodes have been given a priority, but
// unreachable nodes have not been given a priority. Put them last.
// Presumably, it doesn't really matter what priority they get, since
// this shouldn't happen in real code.
for (DiGraphNode candidate : cfg.getNodes()) {
nodePriorities.computeIfAbsent(candidate, k -> ++priorityCounter);
}
// Again, the implicit return node is always last.
nodePriorities.put(cfg.getImplicitReturn(), ++priorityCounter);
}
/**
* Given an entry node, find all the nodes reachable from that node
* and prioritize them.
*/
private void prioritizeFromEntryNode(DiGraphNode entry) {
PriorityQueue> worklist =
new PriorityQueue<>(10, priorityComparator);
worklist.add(entry);
while (!worklist.isEmpty()) {
DiGraphNode current = worklist.remove();
if (nodePriorities.containsKey(current)) {
continue;
}
nodePriorities.put(current, ++priorityCounter);
List> successors = cfg.getDirectedSuccNodes(current);
worklist.addAll(successors);
}
}
@Override
public boolean shouldTraverse(
NodeTraversal nodeTraversal, Node n, Node parent) {
astPosition.put(n, astPositionCounter++);
switch (n.getToken()) {
case FUNCTION:
if (shouldTraverseFunctions || n == cfg.getEntry().getValue()) {
exceptionHandler.push(n);
return true;
}
return false;
case TRY:
exceptionHandler.push(n);
return true;
default:
break;
}
/*
* We are going to stop the traversal depending on what the node's parent
* is.
*
* We are only interested in adding edges between nodes that change control
* flow. The most obvious ones are loops and IF-ELSE's. A statement
* transfers control to its next sibling.
*
* In case of an expression tree, there is no control flow within the tree
* even when there are short circuited operators and conditionals. When we
* are doing data flow analysis, we will simply synthesize lattices up the
* expression tree by finding the meet at each expression node.
*
* For example: within a Token.SWITCH, the expression in question does not
* change the control flow and need not to be considered.
*/
if (parent != null) {
switch (parent.getToken()) {
case FOR:
case FOR_IN:
case FOR_OF:
case FOR_AWAIT_OF:
// Only traverse the body of the for loop.
return n == parent.getLastChild();
case DO:
// Only traverse the body of the do-while.
return n != parent.getSecondChild();
// Skip conditions, and only traverse the body of the cases
case IF:
case WHILE:
case WITH:
case SWITCH:
case CASE:
case CATCH:
case LABEL:
return n != parent.getFirstChild();
case FUNCTION:
return n == parent.getLastChild();
case CLASS:
return shouldTraverseFunctions && n == parent.getLastChild();
case COMPUTED_PROP:
case CONTINUE:
case BREAK:
case EXPR_RESULT:
case VAR:
case LET:
case CONST:
case EXPORT:
case IMPORT:
case RETURN:
case THROW:
return false;
case TRY:
/* When we are done with the TRY block and there is no FINALLY block,
* or done with both the TRY and CATCH block, then no more exceptions
* can be handled at this TRY statement, so it can be taken out of the
* stack.
*/
if ((!NodeUtil.hasFinally(parent) && n == NodeUtil.getCatchBlock(parent))
|| NodeUtil.isTryFinallyNode(parent, n)) {
checkState(exceptionHandler.peek() == parent);
exceptionHandler.pop();
}
break;
case CLASS_MEMBERS:
case MEMBER_FUNCTION_DEF:
default:
break;
}
// Don't traverse further in an arrow function expression
if (parent.getParent() != null
&& parent.getParent().isArrowFunction()
&& !parent.isBlock()) {
return false;
}
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case IF:
handleIf(n);
return;
case WHILE:
handleWhile(n);
return;
case DO:
handleDo(n);
return;
case FOR:
handleFor(n);
return;
case FOR_OF:
case FOR_IN:
case FOR_AWAIT_OF:
handleEnhancedFor(n);
return;
case SWITCH:
handleSwitch(n);
return;
case CASE:
handleCase(n);
return;
case DEFAULT_CASE:
handleDefault(n);
return;
case BLOCK:
case ROOT:
case SCRIPT:
case MODULE_BODY:
handleStmtList(n);
return;
case FUNCTION:
handleFunction(n);
return;
case EXPR_RESULT:
handleExpr(n);
return;
case THROW:
handleThrow(n);
return;
case TRY:
handleTry(n);
return;
case CATCH:
handleCatch(n);
return;
case BREAK:
handleBreak(n);
return;
case CONTINUE:
handleContinue(n);
return;
case RETURN:
handleReturn(n);
return;
case WITH:
handleWith(n);
return;
case LABEL:
case CLASS_MEMBERS:
case MEMBER_FUNCTION_DEF:
return;
default:
handleStmt(n);
return;
}
}
private void handleIf(Node node) {
Node thenBlock = node.getSecondChild();
Node elseBlock = thenBlock.getNext();
createEdge(node, Branch.ON_TRUE, computeFallThrough(thenBlock));
if (elseBlock == null) {
createEdge(node, Branch.ON_FALSE,
computeFollowNode(node, this)); // not taken branch
} else {
createEdge(node, Branch.ON_FALSE, computeFallThrough(elseBlock));
}
connectToPossibleExceptionHandler(
node, NodeUtil.getConditionExpression(node));
}
private void handleWhile(Node node) {
Node cond = node.getFirstChild();
// Control goes to the first statement if the condition evaluates to true.
createEdge(node, Branch.ON_TRUE, computeFallThrough(cond.getNext()));
if (!cond.isTrue()) {
// Control goes to the follow() if the condition evaluates to false.
createEdge(node, Branch.ON_FALSE, computeFollowNode(node, this));
}
connectToPossibleExceptionHandler(
node, NodeUtil.getConditionExpression(node));
}
private void handleDo(Node node) {
Node cond = node.getFirstChild();
// The first edge can be the initial iteration as well as the iterations
// after.
createEdge(node, Branch.ON_TRUE, computeFallThrough(cond));
if (!cond.isTrue()) {
// The edge that leaves the do loop if the condition fails.
createEdge(node, Branch.ON_FALSE, computeFollowNode(node, this));
}
connectToPossibleExceptionHandler(
node, NodeUtil.getConditionExpression(node));
}
private void handleEnhancedFor(Node forNode) {
// We have: for (index in collection) { body }
// or: for (item of collection) { body }
// or: for await (item of collection) { body }
Node item = forNode.getFirstChild();
Node collection = item.getNext();
Node body = collection.getNext();
// The collection behaves like init.
createEdge(collection, Branch.UNCOND, forNode);
// The edge that transfer control to the beginning of the loop body.
createEdge(forNode, Branch.ON_TRUE, computeFallThrough(body));
// The edge to end of the loop.
createEdge(forNode, Branch.ON_FALSE, computeFollowNode(forNode, this));
connectToPossibleExceptionHandler(forNode, collection);
}
private void handleFor(Node forNode) {
// We have for (init; cond; iter) { body }
Node init = forNode.getFirstChild();
Node cond = init.getNext();
Node iter = cond.getNext();
Node body = iter.getNext();
// After initialization, we transfer to the FOR which is in charge of
// checking the condition (for the first time).
createEdge(init, Branch.UNCOND, forNode);
// The edge that transfer control to the beginning of the loop body.
createEdge(forNode, Branch.ON_TRUE, computeFallThrough(body));
// The edge to end of the loop.
if (!cond.isEmpty() && !cond.isTrue()) {
createEdge(forNode, Branch.ON_FALSE, computeFollowNode(forNode, this));
}
// The end of the body will have a unconditional branch to our iter
// (handled by calling computeFollowNode of the last instruction of the
// body. Our iter will jump to the forNode again to another condition
// check.
createEdge(iter, Branch.UNCOND, forNode);
connectToPossibleExceptionHandler(init, init);
connectToPossibleExceptionHandler(forNode, cond);
connectToPossibleExceptionHandler(iter, iter);
}
private void handleSwitch(Node node) {
// Transfer to the first non-DEFAULT CASE. if there are none, transfer
// to the DEFAULT or the EMPTY node.
Node next = getNextSiblingOfType(
node.getSecondChild(), Token.CASE, Token.EMPTY);
if (next != null) { // Has at least one CASE or EMPTY
createEdge(node, Branch.UNCOND, next);
} else { // Has no CASE but possibly a DEFAULT
if (node.getSecondChild() != null) {
createEdge(node, Branch.UNCOND, node.getSecondChild());
} else { // No CASE, no DEFAULT
createEdge(node, Branch.UNCOND, computeFollowNode(node, this));
}
}
connectToPossibleExceptionHandler(node, node.getFirstChild());
}
private void handleCase(Node node) {
// Case is a bit tricky....First it goes into the body if condition is true.
createEdge(node, Branch.ON_TRUE,
node.getSecondChild());
// Look for the next CASE, skipping over DEFAULT.
Node next = getNextSiblingOfType(node.getNext(), Token.CASE);
if (next != null) { // Found a CASE
checkState(next.isCase());
createEdge(node, Branch.ON_FALSE, next);
} else { // No more CASE found, go back and search for a DEFAULT.
Node parent = node.getParent();
Node deflt = getNextSiblingOfType(
parent.getSecondChild(), Token.DEFAULT_CASE);
if (deflt != null) { // Has a DEFAULT
createEdge(node, Branch.ON_FALSE, deflt);
} else { // No DEFAULT found, go to the follow of the SWITCH.
createEdge(node, Branch.ON_FALSE, computeFollowNode(node, this));
}
}
connectToPossibleExceptionHandler(node, node.getFirstChild());
}
private void handleDefault(Node node) {
// Directly goes to the body. It should not transfer to the next case.
createEdge(node, Branch.UNCOND, node.getFirstChild());
}
private void handleWith(Node node) {
// Directly goes to the body. It should not transfer to the next case.
createEdge(node, Branch.UNCOND, node.getLastChild());
connectToPossibleExceptionHandler(node, node.getFirstChild());
}
private void handleStmtList(Node node) {
Node parent = node.getParent();
// Special case, don't add a block of empty CATCH block to the graph.
if (node.isBlock()
&& parent.isTry()
&& NodeUtil.getCatchBlock(parent) == node
&& !NodeUtil.hasCatchHandler(node)) {
return;
}
// A block transfer control to its first child if it is not empty.
Node child = node.getFirstChild();
// Function declarations are skipped since control doesn't go into that
// function (unless it is called)
while (child != null && child.isFunction()) {
child = child.getNext();
}
if (child != null) {
createEdge(node, Branch.UNCOND, computeFallThrough(child));
} else {
createEdge(node, Branch.UNCOND, computeFollowNode(node, this));
}
// Synthetic blocks
if (parent != null) {
switch (parent.getToken()) {
case DEFAULT_CASE:
case CASE:
case TRY:
break;
case ROOT:
// TODO(b/71873602): why is this path necessary?
if (node.isRoot() && node.getNext() != null) {
createEdge(node, Branch.UNCOND, node.getNext());
}
break;
default:
if (node.isBlock() && node.isSyntheticBlock()) {
createEdge(node, Branch.SYN_BLOCK, computeFollowNode(node, this));
}
break;
}
}
}
private void handleFunction(Node node) {
// A block transfer control to its first child if it is not empty.
checkState(node.isFunction());
checkState(node.hasXChildren(3));
createEdge(node, Branch.UNCOND,
computeFallThrough(node.getLastChild()));
checkState(exceptionHandler.peek() == node);
exceptionHandler.pop();
}
private void handleExpr(Node node) {
createEdge(node, Branch.UNCOND, computeFollowNode(node, this));
connectToPossibleExceptionHandler(node, node);
}
private void handleThrow(Node node) {
connectToPossibleExceptionHandler(node, node);
}
private void handleTry(Node node) {
createEdge(node, Branch.UNCOND, node.getFirstChild());
}
private void handleCatch(Node node) {
createEdge(node, Branch.UNCOND, node.getLastChild());
}
private void handleBreak(Node node) {
String label = null;
// See if it is a break with label.
if (node.hasChildren()) {
label = node.getFirstChild().getString();
}
Node cur;
Node previous = null;
Node lastJump;
Node parent = node.getParent();
/*
* Continuously look up the ancestor tree for the BREAK target or the target
* with the corresponding label and connect to it. If along the path we
* discover a FINALLY, we will connect the BREAK to that FINALLY. From then
* on, we will just record the control flow changes in the finallyMap. This
* is due to the fact that we need to connect any node that leaves its own
* FINALLY block to the outer FINALLY or the BREAK's target but those nodes
* are not known yet due to the way we traverse the nodes.
*/
for (cur = node, lastJump = node;
!isBreakTarget(cur, label);
cur = parent, parent = parent.getParent()) {
if (cur.isTry() && NodeUtil.hasFinally(cur)
&& cur.getLastChild() != previous) {
if (lastJump == node) {
createEdge(lastJump, Branch.UNCOND, computeFallThrough(
cur.getLastChild()));
} else {
finallyMap.put(lastJump, computeFallThrough(cur.getLastChild()));
}
lastJump = cur;
}
if (parent == null) {
if (compiler.getOptions().canContinueAfterErrors()) {
// In IDE mode, we expect that the data flow graph may
// not be well-formed.
return;
} else {
throw new IllegalStateException("Cannot find break target.");
}
}
previous = cur;
}
if (lastJump == node) {
createEdge(lastJump, Branch.UNCOND, computeFollowNode(cur, this));
} else {
finallyMap.put(lastJump, computeFollowNode(cur, this));
}
}
private void handleContinue(Node node) {
String label = null;
if (node.hasChildren()) {
label = node.getFirstChild().getString();
}
Node cur;
Node previous = null;
Node lastJump;
// Similar to handBreak's logic with a few minor variation.
for (cur = node, lastJump = node;
!isContinueTarget(cur, label);
cur = cur.getParent()) {
if (cur.isTry() && NodeUtil.hasFinally(cur)
&& cur.getLastChild() != previous) {
if (lastJump == node) {
createEdge(lastJump, Branch.UNCOND, cur.getLastChild());
} else {
finallyMap.put(lastJump, computeFallThrough(cur.getLastChild()));
}
lastJump = cur;
}
checkState(cur.getParent() != null, "Cannot find continue target.");
previous = cur;
}
Node iter = cur;
if (cur.isVanillaFor()) {
// the increment expression happens after the continue
iter = cur.getChildAtIndex(2);
}
if (lastJump == node) {
createEdge(node, Branch.UNCOND, iter);
} else {
finallyMap.put(lastJump, iter);
}
}
private void handleReturn(Node node) {
Node lastJump = null;
for (Node curHandler : exceptionHandler) {
if (curHandler.isFunction()) {
break;
}
if (NodeUtil.hasFinally(curHandler)) {
if (lastJump == null) {
createEdge(node, Branch.UNCOND, curHandler.getLastChild());
} else {
finallyMap.put(lastJump,
computeFallThrough(curHandler.getLastChild()));
}
lastJump = curHandler;
}
}
if (node.hasChildren()) {
connectToPossibleExceptionHandler(node, node.getFirstChild());
}
if (lastJump == null) {
createEdge(node, Branch.UNCOND, null);
} else {
finallyMap.put(lastJump, null);
}
}
private void handleStmt(Node node) {
// Simply transfer to the next line.
createEdge(node, Branch.UNCOND, computeFollowNode(node, this));
connectToPossibleExceptionHandler(node, node);
}
static Node computeFollowNode(Node node, ControlFlowAnalysis cfa) {
return computeFollowNode(node, node, cfa);
}
static Node computeFollowNode(Node node) {
return computeFollowNode(node, node, null);
}
/**
* Computes the follow() node of a given node and its parent. There is a side
* effect when calling this function. If this function computed an edge that
* exists a FINALLY, it'll attempt to connect the fromNode to the outer
* FINALLY according to the finallyMap.
*
* @param fromNode The original source node since {@code node} is changed
* during recursion.
* @param node The node that follow() should compute.
*/
private static Node computeFollowNode(
Node fromNode, Node node, ControlFlowAnalysis cfa) {
/*
* This is the case where:
*
* 1. Parent is null implies that we are transferring control to the end of
* the script.
*
* 2. Parent is a function implies that we are transferring control back to
* the caller of the function.
*
* 3. If the node is a return statement, we should also transfer control
* back to the caller of the function.
*
* 4. If the node is root then we have reached the end of what we have been
* asked to traverse.
*
* In all cases we should transfer control to a "symbolic return" node.
* This will make life easier for DFAs.
*/
Node parent = node.getParent();
if (parent == null || parent.isFunction() ||
(cfa != null && node == cfa.root)) {
return null;
}
// If we are just before a IF/WHILE/DO/FOR:
switch (parent.getToken()) {
// The follow() of any of the path from IF would be what follows IF.
case IF:
return computeFollowNode(fromNode, parent, cfa);
case CASE:
case DEFAULT_CASE:
// After the body of a CASE, the control goes to the body of the next
// case, without having to go to the case condition.
if (parent.getNext() != null) {
if (parent.getNext().isCase()) {
return parent.getNext().getSecondChild();
} else if (parent.getNext().isDefaultCase()) {
return parent.getNext().getFirstChild();
} else {
throw new IllegalStateException("Not reachable");
}
} else {
return computeFollowNode(fromNode, parent, cfa);
}
case FOR_IN:
case FOR_OF:
case FOR_AWAIT_OF:
return parent;
case FOR:
return parent.getSecondChild().getNext();
case WHILE:
case DO:
return parent;
case TRY:
// If we are coming out of the TRY block...
if (parent.getFirstChild() == node) {
if (NodeUtil.hasFinally(parent)) { // and have FINALLY block.
return computeFallThrough(parent.getLastChild());
} else { // and have no FINALLY.
return computeFollowNode(fromNode, parent, cfa);
}
// CATCH block.
} else if (NodeUtil.getCatchBlock(parent) == node){
if (NodeUtil.hasFinally(parent)) { // and have FINALLY block.
return computeFallThrough(node.getNext());
} else {
return computeFollowNode(fromNode, parent, cfa);
}
// If we are coming out of the FINALLY block...
} else if (parent.getLastChild() == node){
if (cfa != null) {
for (Node finallyNode : cfa.finallyMap.get(parent)) {
cfa.createEdge(fromNode, Branch.ON_EX, finallyNode);
}
}
return computeFollowNode(fromNode, parent, cfa);
}
// fall through
default:
break;
}
// Now that we are done with the special cases follow should be its
// immediate sibling, unless its sibling is a function
Node nextSibling = node.getNext();
// Skip function declarations because control doesn't get pass into it.
while (nextSibling != null && nextSibling.isFunction()) {
nextSibling = nextSibling.getNext();
}
if (nextSibling != null) {
return computeFallThrough(nextSibling);
} else {
// If there are no more siblings, control is transferred up the AST.
return computeFollowNode(fromNode, parent, cfa);
}
}
/**
* Computes the destination node of n when we want to fallthrough into the
* subtree of n. We don't always create a CFG edge into n itself because of
* DOs and FORs.
*/
static Node computeFallThrough(Node n) {
switch (n.getToken()) {
case DO:
case FOR:
return computeFallThrough(n.getFirstChild());
case FOR_IN:
case FOR_OF:
case FOR_AWAIT_OF:
return n.getSecondChild();
case LABEL:
return computeFallThrough(n.getLastChild());
default:
return n;
}
}
/**
* Connects the two nodes in the control flow graph.
*
* @param fromNode Source.
* @param toNode Destination.
*/
private void createEdge(Node fromNode, ControlFlowGraph.Branch branch,
Node toNode) {
cfg.createNode(fromNode);
cfg.createNode(toNode);
cfg.connectIfNotFound(fromNode, branch, toNode);
}
/**
* Connects cfgNode to the proper CATCH block if target subtree might throw
* an exception. If there are FINALLY blocks reached before a CATCH, it will
* make the corresponding entry in finallyMap.
*/
private void connectToPossibleExceptionHandler(Node cfgNode, Node target) {
if (mayThrowException(target) && !exceptionHandler.isEmpty()) {
Node lastJump = cfgNode;
for (Node handler : exceptionHandler) {
if (handler.isFunction()) {
return;
}
checkState(handler.isTry());
Node catchBlock = NodeUtil.getCatchBlock(handler);
boolean lastJumpInCatchBlock = false;
for (Node ancestor : lastJump.getAncestors()) {
if (ancestor == handler) {
break;
} else if (ancestor == catchBlock) {
lastJumpInCatchBlock = true;
break;
}
}
// No catch but a FINALLY, or lastJump is inside the catch block.
if (!NodeUtil.hasCatchHandler(catchBlock) || lastJumpInCatchBlock) {
if (lastJump == cfgNode) {
createEdge(cfgNode, Branch.ON_EX, handler.getLastChild());
} else {
finallyMap.put(lastJump, handler.getLastChild());
}
} else { // Has a catch.
if (lastJump == cfgNode) {
createEdge(cfgNode, Branch.ON_EX, catchBlock);
return;
} else {
finallyMap.put(lastJump, catchBlock);
}
}
lastJump = handler;
}
}
}
/**
* Get the next sibling (including itself) of one of the given types.
*/
private static Node getNextSiblingOfType(Node first, Token ... types) {
for (Node c = first; c != null; c = c.getNext()) {
for (Token type : types) {
if (c.getToken() == type) {
return c;
}
}
}
return null;
}
/**
* Checks if target is actually the break target of labeled continue. The
* label can be null if it is an unlabeled break.
*/
public static boolean isBreakTarget(Node target, String label) {
return isBreakStructure(target, label != null) &&
matchLabel(target.getParent(), label);
}
/**
* Checks if target is actually the continue target of labeled continue. The
* label can be null if it is an unlabeled continue.
*/
static boolean isContinueTarget(
Node target, String label) {
return NodeUtil.isLoopStructure(target) && matchLabel(target.getParent(), label);
}
/**
* Check if label is actually referencing the target control structure. If
* label is null, it always returns true.
*/
private static boolean matchLabel(Node target, String label) {
if (label == null) {
return true;
}
while (target.isLabel()) {
if (target.getFirstChild().getString().equals(label)) {
return true;
}
target = target.getParent();
}
return false;
}
/**
* Determines if the subtree might throw an exception.
*/
public static boolean mayThrowException(Node n) {
switch (n.getToken()) {
case CALL:
case TAGGED_TEMPLATELIT:
case GETPROP:
case GETELEM:
case THROW:
case NEW:
case ASSIGN:
case INC:
case DEC:
case INSTANCEOF:
case IN:
case YIELD:
case AWAIT:
return true;
case FUNCTION:
return false;
default:
break;
}
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
if (!ControlFlowGraph.isEnteringNewCfgNode(c) && mayThrowException(c)) {
return true;
}
}
return false;
}
/**
* Determines whether the given node can be terminated with a BREAK node.
*/
static boolean isBreakStructure(Node n, boolean labeled) {
switch (n.getToken()) {
case FOR:
case FOR_IN:
case FOR_OF:
case FOR_AWAIT_OF:
case DO:
case WHILE:
case SWITCH:
return true;
case BLOCK:
case ROOT:
case IF:
case TRY:
return labeled;
default:
return false;
}
}
/**
* Get the TRY block with a CATCH that would be run if n throws an exception.
* @return The CATCH node or null if it there isn't a CATCH before the
* the function terminates.
*/
static Node getExceptionHandler(Node n) {
for (Node cur = n;
!cur.isScript() && !cur.isFunction();
cur = cur.getParent()) {
Node catchNode = getCatchHandlerForBlock(cur);
if (catchNode != null) {
return catchNode;
}
}
return null;
}
/**
* Locate the catch BLOCK given the first block in a TRY.
* @return The CATCH node or null there is no catch handler.
*/
static Node getCatchHandlerForBlock(Node block) {
if (block.isBlock()
&& block.getParent().isTry()
&& block.getParent().getFirstChild() == block) {
for (Node s = block.getNext(); s != null; s = s.getNext()) {
if (NodeUtil.hasCatchHandler(s)) {
return s.getFirstChild();
}
}
}
return null;
}
/**
* A {@link ControlFlowGraph} which provides a node comparator based on the
* pre-order traversal of the AST.
*/
private static class AstControlFlowGraph extends ControlFlowGraph {
private final Map, Integer> priorities;
/**
* Constructor.
* @param entry The entry node.
* @param priorities The map from nodes to position in the AST (to be
* filled by the {@link ControlFlowAnalysis#shouldTraverse}).
*/
private AstControlFlowGraph(Node entry,
Map, Integer> priorities,
boolean edgeAnnotations) {
super(entry,
true /* node annotations */, edgeAnnotations);
this.priorities = priorities;
}
@Override
/**
* Returns a node comparator based on the pre-order traversal of the AST.
* @param isForward x 'before' y in the pre-order traversal implies
* x 'less than' y (if true) and x 'greater than' y (if false).
*/
public Comparator> getOptionalNodeComparator(
boolean isForward) {
if (isForward) {
return comparingInt(this::getPosition);
} else {
return comparingInt(this::getPosition).reversed();
}
}
/**
* Gets the pre-order traversal position of the given node.
* @return An arbitrary counter used for comparing positions.
*/
private int getPosition(DiGraphNode n) {
Integer priority = priorities.get(n);
checkNotNull(priority);
return priority;
}
}
}