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

com.google.javascript.jscomp.ControlFlowAnalysis 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 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
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.IdentityHashMap;
import java.util.List;
import java.util.PriorityQueue;
import org.jspecify.nullness.Nullable;

/**
 * This class computes a {@link ControlFlowGraph} for a given AST
 *
 * 

Example usage: * *

{@code
 * ControlFlowGraph cfg = ControlFlowAnalysis.builder()
 *                                  .setCompiler(compiler)
 *                                  .setCfgRoot(functionRoot)
 *                                  .setShouldIncludeEdgeAnnotations(true)
 *                                  .computeCfg())
 * }
*/ public final class ControlFlowAnalysis implements NodeTraversal.Callback { /** * 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. * *

At the moment, we are not performing any inter-procedural analysis. */ private final AbstractCompiler compiler; private ControlFlowGraph cfg; private @Nullable IdentityHashMap astPosition; // 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 = Comparator.comparingInt( digraphNode -> checkNotNull(astPosition.get(digraphNode.getValue()), 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 SetMultimap finallyMap = HashMultimap.create(); /** * Constructor. Should only be called from within the {@link Builder} * * @param compiler Compiler instance. * @param shouldTraverseFunctions Whether functions should be traversed * @param edgeAnnotations Whether to allow edge annotations. */ private ControlFlowAnalysis( AbstractCompiler compiler, boolean shouldTraverseFunctions, boolean edgeAnnotations) { this.compiler = compiler; this.shouldTraverseFunctions = shouldTraverseFunctions; this.edgeAnnotations = edgeAnnotations; } /** * Configures a {@link ControlFlowAnalysis} instance then computes the {@link ControlFlowGraph} */ public static final class Builder { private AbstractCompiler compiler; private Node cfgRoot; private boolean shouldTraverseFunctions = false; private boolean edgeAnnotations = false; private Builder() {} @CanIgnoreReturnValue public Builder setCompiler(AbstractCompiler compiler) { this.compiler = compiler; return this; } @CanIgnoreReturnValue public Builder setCfgRoot(Node cfgRoot) { this.cfgRoot = cfgRoot; return this; } @CanIgnoreReturnValue public Builder setTraverseFunctions(boolean shouldTraverseFunctions) { this.shouldTraverseFunctions = shouldTraverseFunctions; return this; } @CanIgnoreReturnValue public Builder setIncludeEdgeAnnotations(boolean includeEdgeAnnotations) { this.edgeAnnotations = includeEdgeAnnotations; return this; } public ControlFlowGraph computeCfg() { Preconditions.checkNotNull(compiler, "Need to call setCompiler()"); Preconditions.checkNotNull(cfgRoot, "Need to call setCfgRoot()"); ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, shouldTraverseFunctions, edgeAnnotations); cfa.computeCfg(this.cfgRoot); return cfa.cfg; } } public static Builder builder() { return new Builder(); } ControlFlowGraph getCfg() { return cfg; } private void computeCfg(Node root) { Preconditions.checkArgument( NodeUtil.isValidCfgRoot(root), "Unexpected control flow graph root %s", root); this.root = root; astPosition = new IdentityHashMap<>(); astPositionCounter = 0; cfg = new AstControlFlowGraph(computeFallThrough(root), edgeAnnotations); // Traverse the graph. NodeTraversal.traverse(compiler, root, this); // Insert an implicit return last. astPosition.put(null, ++astPositionCounter); // 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); } } } // Free our large table now that it is no longer needed astPosition = null; // 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()) { if (!candidate.hasPriority()) { candidate.setPriority(++priorityCounter); } } // Again, the implicit return node is always last. cfg.getImplicitReturn().setPriority(++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 (current.hasPriority()) { continue; } current.setPriority(++priorityCounter); List> successors = cfg.getDirectedSuccNodes(current); worklist.addAll(successors); } } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (shouldTraverseIntoChildren(n, parent)) { // Any AST node that will later have a corresponding CFG node must be in astPosition. // To avoid having astPosition grow too large, we exclude AST nodes that are not traversed // further, as they usually are not put in the CFG. astPosition.put(n, astPositionCounter++); return true; } return false; } /** * Returns whether the children of this node should be traversed as part of this control-flow * analysis, i.e. whether the control-flow graph being built may require any edges into children * of this node. */ private boolean shouldTraverseIntoChildren(Node n, Node parent) { 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. boolean shouldTraverseForChild = n == parent.getLastChild(); if (!shouldTraverseForChild) { // The control-flow graph contains edges from a FOR node to all its children, even // though only the body is actually traversed. So put the other children in astPosition. astPosition.put(n, astPositionCounter++); } return shouldTraverseForChild; 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: case MEMBER_FUNCTION_DEF: case MEMBER_FIELD_DEF: case COMPUTED_FIELD_DEF: 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; default: break; } // Don't traverse further in an arrow function expression if (parent.hasParent() && 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: case MEMBER_FIELD_DEF: case COMPUTED_FIELD_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 body = node.getFirstChild(); // The first edge can be the initial iteration as well as the iterations // after. createEdge(node, Branch.ON_TRUE, computeFallThrough(body)); Node cond = body.getNext(); 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.hasParent(), "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 @Nullable Node computeFollowNode( Node fromNode, Node node, @Nullable 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, @Nullable 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 @Nullable 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 function terminates. */ static @Nullable 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 @Nullable 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 final class AstControlFlowGraph extends ControlFlowGraph { /** * Constructs this graph. * * @param entry The entry node. * @param edgeAnnotations Annotation */ private AstControlFlowGraph(Node entry, boolean edgeAnnotations) { super(entry, /* nodeAnnotations = */ true, edgeAnnotations); } /** * 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). */ @Override public Comparator> getOptionalNodeComparator(boolean isForward) { return isForward ? Comparator.>comparingInt(DiGraphNode::getPriority) : Comparator.>comparingInt(DiGraphNode::getPriority).reversed(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy