// Copyright (C) 2008 Google Inc.
//
// 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.caja.ancillary.linter;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.caja.lexer.FilePosition;
import com.google.caja.parser.js.BreakStmt;
import com.google.caja.parser.js.ContinueStmt;
import com.google.caja.parser.js.ReturnStmt;
import com.google.caja.parser.js.Statement;
import com.google.caja.parser.js.ThrowStmt;
import com.google.caja.util.Lists;
import com.google.caja.util.Maps;
import com.google.caja.util.Sets;
/**
* Describes the ways in which execution of a JavaScript parse tree completes.
* <p>
* A block of code can return normally via a {@code return} statement, or it can {@code throw} an exception,
* {@code break} to the end of a containing block, {@code continue} to the beginning of a containing block, or complete
* and pass control to the next statement.
* <p>
* This class describes the ways in which control might leave a block of code. Below, control always leaves via return
*
* <pre>
* if (a) {
* return 1;
* } else {
* return 2;
* }
* </pre>
*
* Sometimes it returns, sometimes it breaks, and sometimes it completes.
*
* <pre>
* if (a) {
* return 2;
* } else if (b) {
* break;
* } // no else
* </pre>
* <p>
* To calculate which variables are live, we need to track variable assignments along all possible flow paths within a
* function. In the first example above, we know that control always returns, so that produces an {@code ExitModes} with
* a single {@code return} {@link ExitModes.ExitMode mode} that is marked as {@link ExitModes.ExitMode#always always}
* exiting. In the second, we have 3 possible exit modes (to account for the implicit {@code else;}, none of which have
* the always bit set.
* <p>
* Associated with each {@code ExitMode} is the set of variables live at the time of exit. That allows us to keep track
* of live variables along paths out of a loop.
*
* <pre>
* var a, b;
* do {
* if (f()) {
* b = 0;
* a = g(); // In this branch, both b and a were set
* } else {
* a = a + 1
* break;
* }
* // Because b was set in all non-exiting branches above, we know that
* // a and b are live here
* ...
* } while (a < 10);
* // Now, when we're done looking at the loop, we can take the live-set for
* // all completing paths (a and b) and intersect it with the live-set at
* // the time of breaks (a) to get the live set here: (a)
* </pre>
*
* <p>
* See also the NOTE in {@link LiveSet}.
*
* @author mikesamuel@gmail.com
*/
final class ExitModes
{
/**
* This is analogous to the <tt>(normal, empty, empty)</tt> triple that is used in Chapter 12 of EcmaScript. It is
* the same as the exit mode of the empty block and the no-op statement.
*/
static final ExitModes COMPLETES = new ExitModes(Collections.<String, ExitMode> emptyMap(), true);
private final Map<String, ExitMode> exits;
private final boolean completes;
/**
* @param completes
* does the AST being described complete instead of breaking, continuing, returning, or throwing along
* all code-paths?
*/
private ExitModes(Map<String, ExitMode> exits, boolean completes) {
this.exits = exits;
this.completes = completes;
}
/** True if execution will leave the current function. */
boolean returns()
{
return returnsNormally() || returnsAbruptly();
}
/**
* True if execution will leave the current function due to a {@code return} statement.
*/
boolean returnsNormally()
{
return hasAlwaysKey("r");
}
/**
* True if execution will leave the current function due to an exception being thrown.
*/
boolean returnsAbruptly()
{
return hasAlwaysKey("t");
}
/**
* The set of variables live at {@code throw} statements.
*
* @return null means that no information is available.
*/
ExitMode atThrow()
{
return exits.get("t");
}
/**
* True if execution will jump to the end of the block with the given label.
*/
boolean breaksToLabel(String label)
{
return hasAlwaysKey(prefix("b", label));
}
/**
* The set of variables live at {@code break} statements for the given label.
*
* @return null means that no information is available.
*/
ExitMode atBreak(String label)
{
return exits.get(prefix("b", label));
}
/**
* True if execution will jump to the start of the block with the given label.
*/
boolean continuesToLabel(String label)
{
return hasAlwaysKey(prefix("b", label));
}
/**
* The set of variables live at {@code break} statements for the given label.
*
* @return null means that no information is available.
*/
ExitMode atContinue(String label)
{
return exits.get(prefix("c", label));
}
/**
* True if execution might continue to the next statement -- there is a code path that does not have a
* {@code return,throw,break,continue}. A return value of true does not imply that there is a next statement.
*/
boolean completes()
{
return completes;
}
Set<Statement> liveExits()
{
List<Statement> stmts = Lists.newArrayList();
for (ExitMode em : exits.values()) {
stmts.addAll(em.sources);
}
// Impose an order on output not dependent on hashing
Collections.sort(stmts, new Comparator<Statement>()
{
public int compare(Statement a, Statement b)
{
FilePosition pa = a.getFilePosition(), pb = b.getFilePosition();
int delta = pa.source().toString().compareTo(pb.source().toString());
if (delta != 0) {
return delta;
}
return pa.startCharInFile() - pb.startCharInFile();
}
});
return Collections.unmodifiableSet(Sets.newLinkedHashSet(stmts));
}
private boolean hasAlwaysKey(String key)
{
ExitMode em = exits.get(key);
return em != null && em.always;
}
/** Same as this, but {@code breaksToLabel(label)}. */
ExitModes withBreak(BreakStmt s, LiveSet atBreak)
{
return withEntry(prefix("b", s.getLabel()), atBreak, s);
}
/** Same as this, but {@code continuesToLabel(label)}. */
ExitModes withContinue(ContinueStmt s, LiveSet atContinue)
{
return withEntry(prefix("c", s.getLabel()), atContinue, s);
}
/** Same as this, but {@code returnsNormally()}. */
ExitModes withNormalReturn(ReturnStmt s, LiveSet atReturn)
{
return withEntry("r", atReturn, s);
}
/** Same as this, but {@code returnsAbruptly()}. */
ExitModes withAbruptReturn(ThrowStmt t, LiveSet atThrow)
{
return withEntry("t", atThrow, t);
}
/** Same as this, but {@code !breaksToLabel(label)}. */
ExitModes withoutBreak(String label)
{
return withoutEntry(prefix("b", label), true);
}
/** Same as this, but {@code !continuesToLabel(label)}. */
ExitModes withoutBreakOrContinue(String label)
{
String b = prefix("b", label), c = prefix("c", label);
int count = (exits.containsKey(b) ? 1 : 0) + (exits.containsKey(c) ? 1 : 0);
if (count == exits.size()) {
return ExitModes.COMPLETES;
}
Map<String, ExitMode> exits = Maps.newLinkedHashMap(this.exits);
boolean completes = exits.remove(b) != null | this.completes;
// The continue does not cause completes -> true since continue does not
// go to the end of a loop.
exits.remove(c);
// The new exit mode completes since a break or continue to the loop in
// question now exits.
return new ExitModes(exits, completes);
}
/** Same as this, but {@code !returnsAbruptly()}. */
ExitModes withoutAbruptReturn()
{
return withoutEntry("t", true);
}
/**
* {@link ExitModes} that are true for any of the predicates above that are true both for this and for m.
*/
ExitModes intersection(ExitModes m)
{
return join(this, m, false);
}
/**
* {@link ExitModes} that are true for any of the predicates above that are true for this or for m.
*/
ExitModes union(ExitModes m)
{
return join(this, m, true);
}
/**
* @param unioning
* true means that an {@code ExitMode} in the output has its {@link ExitMode#always} bit set if at least
* one of {@code (this, m)} has a corresponding {@code ExitMode} with the always bit set. Otherwise, all
* existing corresponding {@code ExitModes} must have the always bit set.
*/
private static ExitModes join(ExitModes a, ExitModes b, boolean unioning)
{
// TODO(mikesamuel): refactor this
if (a == b) {
return a;
}
if (unioning) {
if (a.exits.isEmpty()) {
return b;
}
if (b.exits.isEmpty()) {
return a;
}
}
// Make sure a is not smaller than b.
if (a.exits.size() >= b.exits.size()) {
ExitModes tmp = a;
a = b;
b = tmp;
}
Map<String, ExitMode> exits = Maps.newLinkedHashMap(a.exits);
exits.putAll(b.exits);
boolean same = exits.size() == a.exits.size();
if (unioning) {
for (Map.Entry<String, ExitMode> e : b.exits.entrySet()) {
String k = e.getKey();
ExitMode orig = a.exits.get(k);
if (orig != null) {
ExitMode combined = orig.combine(e.getValue(), true);
if (combined != e.getValue()) {
exits.put(k, combined);
same = false;
}
}
}
} else {
for (Map.Entry<String, ExitMode> e : exits.entrySet()) {
String k = e.getKey();
ExitMode bEl = b.exits.get(k);
if (bEl == null) {
if (e.getValue().always && b.completes) {
e.setValue(e.getValue().sometimes());
same = false;
}
} else {
ExitMode aEl = a.exits.get(k);
ExitMode combined = aEl != null
? aEl.combine(bEl, false)
: a.completes
? bEl.sometimes()
: bEl;
if (combined != aEl) {
e.setValue(combined);
same = false;
}
}
}
}
boolean completes = unioning
// Series of operations only completes if all operations completes.
? a.completes && b.completes
// A set of branches complete if any of the branches complete.
: a.completes || b.completes;
same &= completes == a.completes;
return same ? a : new ExitModes(exits, completes);
}
private static String prefix(String prefix, String suffix)
{
if (suffix == null) {
throw new NullPointerException();
}
if ("".equals(suffix)) {
return prefix;
}
return prefix + suffix;
}
private ExitModes withEntry(String e, LiveSet vars, Statement source)
{
ExitMode em = new ExitMode(vars, true, Collections.singleton(source));
ExitMode orig = exits.get(e);
if (orig != null) {
ExitMode inter = orig.combine(em, false);
if (inter == orig) {
return this;
}
em = inter;
}
Map<String, ExitMode> exits = Maps.newLinkedHashMap(this.exits);
exits.put(e, em);
return new ExitModes(exits, false);
}
private ExitModes withoutEntry(String e, boolean completes)
{
if (!this.exits.containsKey(e)) {
return this;
}
if (this.exits.size() == 1) {
return ExitModes.COMPLETES;
}
Map<String, ExitMode> exits = Maps.newLinkedHashMap(this.exits);
exits.remove(e);
// presumably the program fragment completes because a throw was caught
// or a break matched a loop.
return new ExitModes(exits, completes || this.completes);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof ExitModes)) {
return false;
}
return exits.equals(((ExitModes) o).exits);
}
@Override
public int hashCode()
{
return exits.hashCode();
}
@Override
public String toString()
{
return exits.toString();
}
static final class ExitMode
{
final LiveSet vars;
final boolean always;
final Set<Statement> sources;
/**
* @param vars
* the set of vars live when that exit mode is reached.
* @param always
* true if that exit mode always occurs. In {@code if (x) return false;}, the program returns, but
* not always, whereas in {@code if (x) return true; else return false;} it always returns.
* Unexpected exceptions are not considered for purposes of always.
*/
private ExitMode(LiveSet vars, boolean always, Set<Statement> sources) {
this.vars = vars;
this.always = always;
this.sources = sources;
}
/**
* @param orAlways
* true means that an {@code ExitMode} in the output has its {@link ExitMode#always} bit set if at
* least one of {@code (this, m)} has a corresponding {@code ExitMode} with the always bit set.
* Otherwise, all existing corresponding {@code ExitModes} must have the always bit set.
*/
private ExitMode combine(ExitMode other, boolean orAlways)
{
LiveSet inter = this.vars.intersection(other.vars);
boolean always = orAlways
? this.always || other.always
: this.always && other.always;
if (inter == this.vars && always == this.always) {
return this;
}
if (inter == other.vars && always == other.always) {
return other;
}
Set<Statement> allSources = Sets.newHashSet(this.sources);
allSources.addAll(other.sources);
return new ExitMode(inter, always, allSources);
}
private ExitMode sometimes()
{
return always ? new ExitMode(vars, false, sources) : this;
}
@Override
public int hashCode()
{
return vars.hashCode() ^ (always ? 1 : 0);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof ExitMode)) {
return false;
}
ExitMode that = (ExitMode) o;
return this.vars.equals(that.vars) && this.always == that.always;
}
@Override
public String toString()
{
return "(" + vars + (always ? " always" : "") + ")";
}
}
}