Package com.google.caja.ancillary.linter

Source Code of com.google.caja.ancillary.linter.ExitModes$ExitMode

// 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.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.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" : "") + ")";
    }
  }
}
TOP

Related Classes of com.google.caja.ancillary.linter.ExitModes$ExitMode

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.