Java程序  |  1693行  |  56.83 KB

package annotations.io;

import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import plume.ArraysMDE;
import annotations.util.PersistentStack;

import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LabeledStatementTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.UnionTypeTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.TreePath;

/**
 * A path through the AST.
 */
public class ASTPath extends ConsStack<ASTPath.ASTEntry>
implements Comparable<ASTPath>, Iterable<ASTPath.ASTEntry> {
  private static final ASTPath EMPTY = new ASTPath();
  private static final String[] typeSelectors =
    { "bound", "identifier", "type", "typeAlternative", "typeArgument",
    "typeParameter", "underlyingType" };

  // Constants for the various child selectors.
  public static final String ANNOTATION = "annotation";
  public static final String ARGUMENT = "argument";
  public static final String BLOCK = "block";
  public static final String BODY = "body";
  public static final String BOUND = "bound";
  public static final String CASE = "case";
  public static final String CATCH = "catch";
  public static final String CLASS_BODY = "classBody";
  public static final String CONDITION = "condition";
  public static final String DETAIL = "detail";
  public static final String DIMENSION = "dimension";
  public static final String ELSE_STATEMENT = "elseStatement";
  public static final String ENCLOSING_EXPRESSION = "enclosingExpression";
  public static final String EXPRESSION = "expression";
  public static final String FALSE_EXPRESSION = "falseExpression";
  public static final String FINALLY_BLOCK = "finallyBlock";
  public static final String IDENTIFIER = "identifier";
  public static final String INDEX = "index";
  public static final String INITIALIZER = "initializer";
  public static final String LEFT_OPERAND = "leftOperand";
  public static final String METHOD_SELECT = "methodSelect";
  public static final String MODIFIERS = "modifiers";
  public static final String PARAMETER = "parameter";
  public static final String QUALIFIER_EXPRESSION = "qualifierExpression";
  public static final String RESOURCE = "resource";
  public static final String RIGHT_OPERAND = "rightOperand";
  public static final String STATEMENT = "statement";
  public static final String THEN_STATEMENT = "thenStatement";
  public static final String THROWS = "throws";
  public static final String TRUE_EXPRESSION = "trueExpression";
  public static final String TYPE = "type";
  public static final String TYPE_ALTERNATIVE = "typeAlternative";
  public static final String TYPE_ARGUMENT = "typeArgument";
  public static final String TYPE_PARAMETER = "typeParameter";
  public static final String UNDERLYING_TYPE = "underlyingType";
  public static final String UPDATE = "update";
  public static final String VARIABLE = "variable";

  /**
   * A single entry in an AST path.
   */
  public static class ASTEntry implements Comparable<ASTEntry> {
    private Tree.Kind treeKind;
    private String childSelector;
    private Integer argument;

    /**
     * Constructs a new AST entry. For example, in the entry:
     * <pre>
     * {@code
     * Block.statement 3
     * }</pre>
     * the tree kind is "Block", the child selector is "statement", and the
     * argument is "3".
     *
     * @param treeKind the kind of this AST entry
     * @param childSelector the child selector to this AST entry
     * @param argument the argument
     */
    public ASTEntry(Tree.Kind treeKind, String childSelector, Integer argument) {
      this.treeKind = treeKind;
      this.childSelector = childSelector;
      this.argument = argument;
    }

    /**
     * Constructs a new AST entry, without an argument.
     *
     * See {@link #ASTEntry(Tree.Kind, String, Integer)} for an example of the parameters.
     *
     * @param treeKind the kind of this AST entry
     * @param childSelector the child selector to this AST entry
     */
    public ASTEntry(Tree.Kind treeKind, String childSelector) {
      this(treeKind, childSelector, null);
    }

    /**
     * Gets the tree node equivalent kind of this AST entry. For example, in
     * <pre>
     * {@code
     * Block.statement 3
     * }</pre>
     * "Block" is the tree kind.
     * @return the tree kind
     */
    public Tree.Kind getTreeKind() {
      return treeKind;
    }

    /**
     * Gets the child selector of this AST entry. For example, in
     * <pre>
     * {@code
     * Block.statement 3
     * }</pre>
     * "statement" is the child selector.
     * @return the child selector
     */
    public String getChildSelector() {
      return childSelector;
    }

    /**
     * Determines if the given string is equal to this AST path entry's
     * child selector.
     *
     * @param s the string to compare to
     * @return {@code true} if the string matches the child selector,
     *         {@code false} otherwise.
     */
    public boolean childSelectorIs(String s) {
      return childSelector.equals(s);
    }

    /**
     * Gets the argument of this AST entry. For example, in
     * <pre>
     * {@code
     * Block.statement 3
     * }</pre>
     * "3" is the argument.
     * @return the argument
     * @throws IllegalStateException if this AST entry does not have an argument
     */
    public int getArgument() {
      if (argument >= (negativeAllowed() ? -1 : 0)) {
        return argument;
      }
      throw new IllegalStateException("Value not set.");
    }

    /**
     * Checks that this Entry has an argument.
     *
     * @return if this entry has an argument
     */
    public boolean hasArgument() {
      return argument == null ? false
          : argument >= 0 ? true
          : negativeAllowed();
    }

    // argument < 0 valid for two cases
    private boolean negativeAllowed() {
      switch (treeKind) {
      case CLASS:
        return childSelectorIs(ASTPath.BOUND);
      case METHOD:
        return childSelectorIs(ASTPath.PARAMETER);
      default:
        return false;
      }
    }

    @Override
    public int compareTo(ASTEntry o) {
      if (o == null) {
        return 1;
      } else if (o.childSelector == null) {
        if (childSelector != null) { return 1; }
      } else if (childSelector == null) {
        return -1;
      }
      int c = treeKind.compareTo(o.treeKind);
      if (c != 0) { return c; }
      c = childSelector.compareTo(o.childSelector);
      if (c != 0) { return c; }
      return o.argument == null
          ? argument == null ? 0 : 1
          : argument == null ? -1 : argument.compareTo(o.argument);
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof ASTEntry && compareTo((ASTEntry) o) == 0;
    }

    @Override
    public int hashCode() {
      int base = treeKind.hashCode() ^ childSelector.hashCode();
      int shift = argument == null ? 0 : 2 + argument;
      return Integer.rotateRight(base, shift);
    }

    @Override
    public String toString() {
      StringBuilder b = new StringBuilder();
      switch (treeKind) {
      case CLASS:
      case ENUM:
      case INTERFACE:
        b.append("Class");
        break;
      case AND:
      case CONDITIONAL_AND:
      case CONDITIONAL_OR:
      case DIVIDE:
      case EQUAL_TO:
      case GREATER_THAN:
      case GREATER_THAN_EQUAL:
      case LEFT_SHIFT:
      case LESS_THAN:
      case LESS_THAN_EQUAL:
      case MINUS:
      case MULTIPLY:
      case NOT_EQUAL_TO:
      case OR:
      case PLUS:
      case REMAINDER:
      case RIGHT_SHIFT:
      case XOR:
        b.append("Binary");
        break;
      case LOGICAL_COMPLEMENT:
      case POSTFIX_DECREMENT:
      case POSTFIX_INCREMENT:
      case PREFIX_DECREMENT:
      case PREFIX_INCREMENT:
      case UNARY_MINUS:
      case UNARY_PLUS:
      case UNSIGNED_RIGHT_SHIFT:
        b.append("Unary");
        break;
      case AND_ASSIGNMENT:
      case DIVIDE_ASSIGNMENT:
      case LEFT_SHIFT_ASSIGNMENT:
      case MINUS_ASSIGNMENT:
      case MULTIPLY_ASSIGNMENT:
      case OR_ASSIGNMENT:
      case PLUS_ASSIGNMENT:
      case REMAINDER_ASSIGNMENT:
      case RIGHT_SHIFT_ASSIGNMENT:
      case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT:
      case XOR_ASSIGNMENT:
        b.append("CompoundAssignment");
        break;
      case EXTENDS_WILDCARD:
      case SUPER_WILDCARD:
      case UNBOUNDED_WILDCARD:
        b.append("Wildcard");
        break;
      case ANNOTATION:
      case TYPE_ANNOTATION:
        b.append("Annotation");
        break;
      default:
        String s = treeKind.toString();
        int n = s.length();
        boolean cap = true;  // capitalize next character
        for (int i = 0; i < n; i++) {
          char c = s.charAt(i);
          if (c == '_') {
            cap = true;
          } else {
            b.append(cap ? Character.toUpperCase(c)
                : Character.toLowerCase(c));
            cap = false;
          }
        }
      }
      b.append(".").append(childSelector);
      if (argument != null) { b.append(" ").append(argument); }
      return b.toString();
    }
  }

  private static Comparator<ASTPath> comparator = new Comparator<ASTPath>() {
    @Override
    public int compare(ASTPath p1, ASTPath p2) {
      return p1 == null ? (p2 == null ? 0 : -1) : p1.compareTo(p2);
    }
  };

  ASTPath() {}

  public static ASTPath empty() { return EMPTY; }

  public static Comparator<ASTPath> getComparator() {
    return comparator;
  }

  // TODO: replace w/ skip list?
  @Override
  public Iterator<ASTEntry> iterator() {
    PersistentStack<ASTEntry> s = this;
    int n = size();
    ASTEntry[] a = new ASTEntry[n];
    while (--n >= 0) {
      a[n] = s.peek();
      s = s.pop();
    }
    return Arrays.asList(a).iterator();
  }

  public ASTPath extendNewArray(int depth) {
    return extend(new ASTEntry(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, depth));
  }

  public ASTPath newArrayLevel(int depth) {
    return add(new ASTEntry(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, depth));
  }

  public ASTPath add(ASTEntry entry) {
    ASTPath path = EMPTY;
    for (ASTEntry e : this) { path = path.extend(e); }
    return path.extend(entry);
  }

  public ASTPath extend(ASTEntry entry) {
    return (ASTPath) push(entry);
  }

  public ASTPath getParentPath() {
    return (ASTPath) pop();
  }

  public ASTEntry get(int index) {
    PersistentStack<ASTEntry> s = this;
    int n = size();
    if (index >= n) {
      throw new NoSuchElementException(Integer.toString(index));
    }
    if (index < 0) {
      index += n;
      if (index < 0) {
        throw new IllegalArgumentException("negative index " + index);
      }
    }
    while (--n > index) { s = s.pop(); }
    return s.peek();
  }

  @Override
  public int hashCode() {
    // hacky fix: remove {Method,Class}.body for comparison
    PersistentStack<ASTEntry> s = canonical(this);
    int hash = 0;
    while (!s.isEmpty()) {
      hash = Integer.rotateRight(hash ^ s.peek().hashCode(), 1);
      s = s.pop();
    }
    return hash;
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof ASTPath && equals((ASTPath) o);
  }

  public boolean equals(ASTPath astPath) {
    return compareTo(astPath) == 0;
  }

  @Override
  public int compareTo(ASTPath o) {
    // hacky fix: remove {Method,Class}.body for comparison
    PersistentStack<ASTEntry> s0 = canonical(this);
    PersistentStack<ASTEntry> s1 = canonical(o);
    Deque<ASTEntry> d0 = new LinkedList<ASTEntry>();
    Deque<ASTEntry> d1 = new LinkedList<ASTEntry>();
    int c = 0;
    while (!s0.isEmpty()) {
      d0.push(s0.peek());
      s0 = s0.pop();
    }
    while (!s1.isEmpty()) {
      d1.push(s1.peek());
      s1 = s1.pop();
    }
    int n0 = d0.size();
    int n1 = d1.size();
    c = Integer.compare(n0, n1);
    if (c == 0) {
      Iterator<ASTEntry> i0 = d0.iterator();
      Iterator<ASTEntry> i1 = d1.iterator();
      while (i0.hasNext()) {
        c = i0.next().compareTo(i1.next());
        if (c != 0) { return c; }
      }
    }
    return c;
  }

  private static ASTPath canonical(ASTPath astPath) {
    // TODO
    return astPath;
  }

  @Override
  public String toString() {
    if (isEmpty()) { return ""; }
    Iterator<ASTEntry> iter = iterator();
    StringBuilder sb = new StringBuilder().append(iter.next());
    while (iter.hasNext()) {
      sb = sb.append(", ").append(iter.next());
    }
    return sb.toString();
  }

  /**
   * Create a new {@code ASTPath} from a formatted string description.
   *
   * @param s formatted string as in JAIF {@code insert-\{cast,annotation\}}
   * @return the corresponding {@code ASTPath}
   * @throws ParseException
   */
  public static ASTPath parse(final String s) throws ParseException {
    return new Parser(s).parseASTPath();
  }

  /**
   * Determine whether this {@code ASTPath} matches a given {@code TreePath}.
   */
  public boolean matches(TreePath treePath) {
    CompilationUnitTree cut = treePath.getCompilationUnit();
    Tree leaf = treePath.getLeaf();
    ASTPath astPath = ASTIndex.indexOf(cut).get(leaf).astPath;  // FIXME
    return this.equals(astPath);
  }

  static class Parser {
    // adapted from annotations.io.IndexFileParser
    // TODO: refactor IndexFileParser to use this class

    StreamTokenizer st;

    Parser(String s) {
      st = new StreamTokenizer(new StringReader(s));
    }

    private void getTok() {
      try {
        st.nextToken();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    private boolean gotType(int t) {
      return st.ttype == t;
    }

    private int intVal() throws ParseException {
      if (gotType(StreamTokenizer.TT_NUMBER)) {
        int n = (int) st.nval;
        if (n == st.nval) {
          return n;
        }
      }
      throw new ParseException("expected integer, got " + st);
    }

    private String strVal() throws ParseException {
      if (gotType(StreamTokenizer.TT_WORD)) {
        return st.sval;
      }
      throw new ParseException("expected string, got " + st);
    }

    /**
     * Parses an AST path.
     * @return the AST path
     */
    ASTPath parseASTPath() throws ParseException {
      ASTPath astPath = new ASTPath().extend(parseASTEntry());
      while (gotType(',')) {
        getTok();
        astPath = astPath.extend(parseASTEntry());
      }
      return astPath;
    }

    /**
     * Parses and returns the next AST entry.
     * @return a new AST entry
     * @throws ParseException if the next entry type is invalid
     */
    ASTEntry parseASTEntry() throws ParseException {
      String s = strVal();

      if (s.equals("AnnotatedType")) {
        return newASTEntry(Tree.Kind.ANNOTATED_TYPE,
            new String[] {ASTPath.ANNOTATION, ASTPath.UNDERLYING_TYPE},
            new String[] {ASTPath.ANNOTATION});
      } else if (s.equals("ArrayAccess")) {
        return newASTEntry(Tree.Kind.ARRAY_ACCESS,
            new String[] {ASTPath.EXPRESSION, ASTPath.INDEX});
      } else if (s.equals("ArrayType")) {
        return newASTEntry(Tree.Kind.ARRAY_TYPE,
            new String[] {ASTPath.TYPE});
      } else if (s.equals("Assert")) {
        return newASTEntry(Tree.Kind.ASSERT,
            new String[] {ASTPath.CONDITION, ASTPath.DETAIL});
      } else if (s.equals("Assignment")) {
        return newASTEntry(Tree.Kind.ASSIGNMENT,
            new String[] {ASTPath.VARIABLE, ASTPath.EXPRESSION});
      } else if (s.equals("Binary")) {
        // Always use Tree.Kind.PLUS for Binary
        return newASTEntry(Tree.Kind.PLUS,
            new String[] {ASTPath.LEFT_OPERAND, ASTPath.RIGHT_OPERAND});
      } else if (s.equals("Block")) {
        return newASTEntry(Tree.Kind.BLOCK,
            new String[] {ASTPath.STATEMENT},
            new String[] {ASTPath.STATEMENT});
      } else if (s.equals("Case")) {
        return newASTEntry(Tree.Kind.CASE,
            new String[] {ASTPath.EXPRESSION, ASTPath.STATEMENT},
            new String[] {ASTPath.STATEMENT});
      } else if (s.equals("Catch")) {
        return newASTEntry(Tree.Kind.CATCH,
            new String[] {ASTPath.PARAMETER, ASTPath.BLOCK});
      } else if (s.equals("Class")) {
        return newASTEntry(Tree.Kind.CLASS,
            new String[] {ASTPath.BOUND, ASTPath.INITIALIZER,
                ASTPath.TYPE_PARAMETER},
            new String[] {ASTPath.BOUND, ASTPath.INITIALIZER,
                ASTPath.TYPE_PARAMETER});
      } else if (s.equals("CompoundAssignment")) {
        // Always use Tree.Kind.PLUS_ASSIGNMENT for CompoundAssignment
        return newASTEntry(Tree.Kind.PLUS_ASSIGNMENT,
            new String[] {ASTPath.VARIABLE, ASTPath.EXPRESSION});
      } else if (s.equals("ConditionalExpression")) {
        return newASTEntry(Tree.Kind.CONDITIONAL_EXPRESSION,
            new String[] {ASTPath.CONDITION,
                ASTPath.TRUE_EXPRESSION,
                ASTPath.FALSE_EXPRESSION});
      } else if (s.equals("DoWhileLoop")) {
        return newASTEntry(Tree.Kind.DO_WHILE_LOOP,
            new String[] {ASTPath.CONDITION, ASTPath.STATEMENT});
      } else if (s.equals("EnhancedForLoop")) {
        return newASTEntry(Tree.Kind.ENHANCED_FOR_LOOP,
            new String[] {ASTPath.VARIABLE,
                ASTPath.EXPRESSION,
                ASTPath.STATEMENT});
      } else if (s.equals("ExpressionStatement")) {
        return newASTEntry(Tree.Kind.EXPRESSION_STATEMENT,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("ForLoop")) {
        return newASTEntry(Tree.Kind.FOR_LOOP,
            new String[] {ASTPath.INITIALIZER, ASTPath.CONDITION,
                ASTPath.UPDATE, ASTPath.STATEMENT},
            new String[] {ASTPath.INITIALIZER, ASTPath.UPDATE});
      } else if (s.equals("If")) {
        return newASTEntry(Tree.Kind.IF,
            new String[] {ASTPath.CONDITION,
                ASTPath.THEN_STATEMENT, ASTPath.ELSE_STATEMENT});
      } else if (s.equals("InstanceOf")) {
        return newASTEntry(Tree.Kind.INSTANCE_OF,
            new String[] {ASTPath.EXPRESSION, ASTPath.TYPE});
      } else if (s.equals("LabeledStatement")) {
        return newASTEntry(Tree.Kind.LABELED_STATEMENT,
            new String[] {ASTPath.STATEMENT});
      } else if (s.equals("LambdaExpression")) {
        return newASTEntry(Tree.Kind.LAMBDA_EXPRESSION,
            new String[] {ASTPath.PARAMETER, ASTPath.BODY},
            new String[] {ASTPath.PARAMETER});
      } else if (s.equals("MemberReference")) {
        return newASTEntry(Tree.Kind.MEMBER_REFERENCE,
            new String[] {ASTPath.QUALIFIER_EXPRESSION, ASTPath.TYPE_ARGUMENT},
            new String[] {ASTPath.TYPE_ARGUMENT});
      } else if (s.equals("MemberSelect")) {
        return newASTEntry(Tree.Kind.MEMBER_SELECT,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("Method")) {
        return newASTEntry(Tree.Kind.METHOD,
            new String[] {ASTPath.BODY, ASTPath.PARAMETER,
                ASTPath.TYPE, ASTPath.TYPE_PARAMETER},
            new String[] {ASTPath.PARAMETER, ASTPath.TYPE_PARAMETER});
      } else if (s.equals("MethodInvocation")) {
        return newASTEntry(Tree.Kind.METHOD_INVOCATION,
            new String[] {ASTPath.TYPE_ARGUMENT,
                ASTPath.METHOD_SELECT, ASTPath.ARGUMENT},
            new String[] {ASTPath.TYPE_ARGUMENT, ASTPath.ARGUMENT});
      } else if (s.equals("NewArray")) {
        return newASTEntry(Tree.Kind.NEW_ARRAY,
            new String[] {ASTPath.TYPE, ASTPath.DIMENSION,
                ASTPath.INITIALIZER},
            new String[] {ASTPath.TYPE, ASTPath.DIMENSION,
                ASTPath.INITIALIZER});
      } else if (s.equals("NewClass")) {
        return newASTEntry(Tree.Kind.NEW_CLASS,
            new String[] {ASTPath.ENCLOSING_EXPRESSION,
                ASTPath.TYPE_ARGUMENT, ASTPath.IDENTIFIER,
                ASTPath.ARGUMENT, ASTPath.CLASS_BODY},
            new String[] {ASTPath.TYPE_ARGUMENT, ASTPath.ARGUMENT});
      } else if (s.equals("ParameterizedType")) {
        return newASTEntry(Tree.Kind.PARAMETERIZED_TYPE,
            new String[] {ASTPath.TYPE, ASTPath.TYPE_ARGUMENT},
            new String[] {ASTPath.TYPE_ARGUMENT});
      } else if (s.equals("Parenthesized")) {
        return newASTEntry(Tree.Kind.PARENTHESIZED,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("Return")) {
        return newASTEntry(Tree.Kind.RETURN,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("Switch")) {
        return newASTEntry(Tree.Kind.SWITCH,
            new String[] {ASTPath.EXPRESSION, ASTPath.CASE},
            new String[] {ASTPath.CASE});
      } else if (s.equals("Synchronized")) {
        return newASTEntry(Tree.Kind.SYNCHRONIZED,
            new String[] {ASTPath.EXPRESSION, ASTPath.BLOCK});
      } else if (s.equals("Throw")) {
        return newASTEntry(Tree.Kind.THROW,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("Try")) {
        return newASTEntry(Tree.Kind.TRY,
            new String[] {ASTPath.BLOCK, ASTPath.CATCH, ASTPath.FINALLY_BLOCK},
            new String[] {ASTPath.CATCH});
      } else if (s.equals("TypeCast")) {
        return newASTEntry(Tree.Kind.TYPE_CAST,
            new String[] {ASTPath.TYPE, ASTPath.EXPRESSION});
      } else if (s.equals("Unary")) {
        // Always use Tree.Kind.UNARY_PLUS for Unary
        return newASTEntry(Tree.Kind.UNARY_PLUS,
            new String[] {ASTPath.EXPRESSION});
      } else if (s.equals("UnionType")) {
        return newASTEntry(Tree.Kind.UNION_TYPE,
            new String[] {ASTPath.TYPE_ALTERNATIVE},
            new String[] {ASTPath.TYPE_ALTERNATIVE});
      } else if (s.equals("Variable")) {
        return newASTEntry(Tree.Kind.VARIABLE,
            new String[] {ASTPath.TYPE, ASTPath.INITIALIZER});
      } else if (s.equals("WhileLoop")) {
        return newASTEntry(Tree.Kind.WHILE_LOOP,
            new String[] {ASTPath.CONDITION, ASTPath.STATEMENT});
      } else if (s.equals("Wildcard")) {
        // Always use Tree.Kind.UNBOUNDED_WILDCARD for Wildcard
        return newASTEntry(Tree.Kind.UNBOUNDED_WILDCARD,
            new String[] {ASTPath.BOUND});
      }

      throw new ParseException("Invalid AST path type: " + s);
    }

    /**
     * Parses and constructs a new AST entry, where none of the child selections require
     * arguments. For example, the call:
     *
     * <pre>
     * {@code newASTEntry(Tree.Kind.WHILE_LOOP, new String[] {"condition", "statement"});</pre>
     *
     * constructs a while loop AST entry, where the valid child selectors are "condition" or
     * "statement".
     *
     * @param kind the kind of this AST entry
     * @param legalChildSelectors a list of the legal child selectors for this AST entry
     * @return a new {@link ASTEntry}
     * @throws ParseException if an illegal argument is found
     */
    private ASTEntry newASTEntry(Tree.Kind kind, String[] legalChildSelectors)
        throws ParseException {
      return newASTEntry(kind, legalChildSelectors, null);
    }

    /**
     * Parses and constructs a new AST entry. For example, the call:
     *
     * <pre>
     * {@code newASTEntry(Tree.Kind.CASE, new String[] {"expression", "statement"}, new String[] {"statement"});
     * </pre>
     *
     * constructs a case AST entry, where the valid child selectors are
     * "expression" or "statement" and the "statement" child selector requires
     * an argument.
     *
     * @param kind the kind of this AST entry
     * @param legalChildSelectors a list of the legal child selectors for this AST entry
     * @param argumentChildSelectors a list of the child selectors that also require an argument.
     *                               Entries here should also be in the legalChildSelectors list.
     * @return a new {@link ASTEntry}
     * @throws ParseException if an illegal argument is found
     */
    private ASTEntry newASTEntry(Tree.Kind kind, String[] legalChildSelectors,
        String[] argumentChildSelectors) throws ParseException {
      if (gotType('.')) {
        getTok();
      } else {
        throw new ParseException("expected '.', got " + st);
      }

      String s = strVal();
      for (String arg : legalChildSelectors) {
        if (s.equals(arg)) {
          if (argumentChildSelectors != null
              && ArraysMDE.indexOf(argumentChildSelectors, arg) >= 0) {
            getTok();
            return new ASTEntry(kind, arg, intVal());
          } else {
            return new ASTEntry(kind, arg);
          }
        }
      }

      throw new ParseException("Invalid argument for " + kind
          + " (legal arguments - " + Arrays.toString(legalChildSelectors)
          + "): " + s);
    }
  }

  static class Matcher {
    // adapted from IndexFileParser.parseASTPath et al.
    // TODO: refactor switch statement into TreeVisitor?
    public static final DebugWriter dbug = new DebugWriter();
    private ASTPath astPath;

    Matcher(ASTPath astPath) {
      this.astPath = astPath;
    }

    private boolean nonDecl(TreePath path) {
      switch (path.getLeaf().getKind()) {
      case CLASS:
      case METHOD:
        return false;
      case VARIABLE:
        TreePath parentPath = path.getParentPath();
        return parentPath != null
            && parentPath.getLeaf().getKind() != Tree.Kind.CLASS;
      default:
        return true;
      }
    }

    public boolean matches(TreePath path) {
      return matches(path, -1);
    }

    public boolean matches(TreePath path, int depth) {
      if (path == null) {
        return false;
      }

      // actualPath stores the path through the source code AST to this
      // location (specified by the "path" parameter to this method). It is
      // computed by traversing from this location up the source code AST
      // until it reaches a method node (this gets only the part of the path
      // within a method) or class node (this gets only the part of the path
      // within a field).
      List<Tree> actualPath = new ArrayList<Tree>();
      while (path != null && nonDecl(path)) {
        actualPath.add(0, path.getLeaf());
        path = path.getParentPath();
      }

      if (dbug.isEnabled()) {
        dbug.debug("AST [%s]%n", astPath);
        for (Tree t : actualPath) {
          dbug.debug("  %s: %s%n", t.getKind(),
              t.toString().replace('\n', ' '));
        }
      }

      if (astPath.isEmpty() || actualPath.isEmpty()
          || actualPath.size() != astPath.size() + 1) {
        return false;
      }

      for (int i = 0; i < astPath.size() && i < actualPath.size(); i++) {
        ASTPath.ASTEntry astNode = astPath.get(i);
        Tree actualNode = actualPath.get(i);

        // Based on the child selector and (optional) argument in "astNode",
        // "next" will get set to the next source node below "actualNode".
        // Then "next" will be compared with the node following "astNode"
        // in "actualPath". If it's not a match, this is not the correct
        // location. If it is a match, keep going.
        Tree next = null;
        dbug.debug("astNode: %s%n", astNode);
        dbug.debug("actualNode: %s%n", actualNode.getKind());
        if (!kindsMatch(astNode.getTreeKind(), actualNode.getKind())) {
          return false;
        }

        switch (actualNode.getKind()) {
        case ANNOTATED_TYPE: {
          AnnotatedTypeTree annotatedType = (AnnotatedTypeTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.ANNOTATION)) {
            int arg = astNode.getArgument();
            List<? extends AnnotationTree> annos = annotatedType.getAnnotations();
            if (arg >= annos.size()) {
              return false;
            }
            next = annos.get(arg);
          } else {
            next = annotatedType.getUnderlyingType();
          }
          break;
        }
        case ARRAY_ACCESS: {
          ArrayAccessTree arrayAccess = (ArrayAccessTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = arrayAccess.getExpression();
          } else {
            next = arrayAccess.getIndex();
          }
          break;
        }
        case ARRAY_TYPE: {
          ArrayTypeTree arrayType = (ArrayTypeTree) actualNode;
          next = arrayType.getType();
          break;
        }
        case ASSERT: {
          AssertTree azzert = (AssertTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = azzert.getCondition();
          } else {
            next = azzert.getDetail();
          }
          break;
        }
        case ASSIGNMENT: {
          AssignmentTree assignment = (AssignmentTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.VARIABLE)) {
            next = assignment.getVariable();
          } else {
            next = assignment.getExpression();
          }
          break;
        }
        case BLOCK: {
          BlockTree block = (BlockTree) actualNode;
          int arg = astNode.getArgument();
          List<? extends StatementTree> statements = block.getStatements();
          if (arg >= block.getStatements().size()) {
            return false;
          }
          next = statements.get(arg);
          break;
        }
        case CASE: {
          CaseTree caze = (CaseTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = caze.getExpression();
          } else {
            int arg = astNode.getArgument();
            List<? extends StatementTree> statements = caze.getStatements();
            if (arg >= statements.size()) {
              return false;
            }
            next = statements.get(arg);
          }
          break;
        }
        case CATCH: {
          CatchTree cach = (CatchTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.PARAMETER)) {
            next = cach.getParameter();
          } else {
            next = cach.getBlock();
          }
          break;
        }
        case CLASS: {
          ClassTree clazz = (ClassTree) actualNode;
          int arg = astNode.getArgument();
          if (astNode.childSelectorIs(ASTPath.BOUND)) {
            next = arg == -1 ? clazz.getExtendsClause()
                : clazz.getImplementsClause().get(arg);
          } else {
            next = clazz.getTypeParameters().get(arg);
          }
          break;
        }
        case CONDITIONAL_EXPRESSION: {
          ConditionalExpressionTree conditionalExpression =
              (ConditionalExpressionTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = conditionalExpression.getCondition();
          } else if (astNode.childSelectorIs(ASTPath.TRUE_EXPRESSION)) {
            next = conditionalExpression.getTrueExpression();
          } else {
            next = conditionalExpression.getFalseExpression();
          }
          break;
        }
        case DO_WHILE_LOOP: {
          DoWhileLoopTree doWhileLoop =(DoWhileLoopTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = doWhileLoop.getCondition();
          } else {
            next = doWhileLoop.getStatement();
          }
          break;
        }
        case ENHANCED_FOR_LOOP: {
          EnhancedForLoopTree enhancedForLoop = (EnhancedForLoopTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.VARIABLE)) {
            next = enhancedForLoop.getVariable();
          } else if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = enhancedForLoop.getExpression();
          } else {
            next = enhancedForLoop.getStatement();
          }
          break;
        }
        case EXPRESSION_STATEMENT: {
          ExpressionStatementTree expressionStatement =
              (ExpressionStatementTree) actualNode;
          next = expressionStatement.getExpression();
          break;
        }
        case FOR_LOOP: {
          ForLoopTree forLoop = (ForLoopTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.INITIALIZER)) {
            int arg = astNode.getArgument();
            List<? extends StatementTree> inits = forLoop.getInitializer();
            if (arg >= inits.size()) {
              return false;
            }
            next = inits.get(arg);
          } else if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = forLoop.getCondition();
          } else if (astNode.childSelectorIs(ASTPath.UPDATE)) {
            int arg = astNode.getArgument();
            List<? extends ExpressionStatementTree> updates = forLoop.getUpdate();
            if (arg >= updates.size()) {
              return false;
            }
            next = updates.get(arg);
          } else {
            next = forLoop.getStatement();
          }
          break;
        }
        case IF: {
          IfTree iff = (IfTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = iff.getCondition();
          } else if (astNode.childSelectorIs(ASTPath.THEN_STATEMENT)) {
            next = iff.getThenStatement();
          } else {
            next = iff.getElseStatement();
          }
          break;
        }
        case INSTANCE_OF: {
          InstanceOfTree instanceOf = (InstanceOfTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = instanceOf.getExpression();
          } else {
            next = instanceOf.getType();
          }
          break;
        }
        case LABELED_STATEMENT: {
          LabeledStatementTree labeledStatement =
              (LabeledStatementTree) actualNode;
          next = labeledStatement.getStatement();
          break;
        }
        case LAMBDA_EXPRESSION: {
          LambdaExpressionTree lambdaExpression =
              (LambdaExpressionTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.PARAMETER)) {
            int arg = astNode.getArgument();
            List<? extends VariableTree> params =
                lambdaExpression.getParameters();
            if (arg >= params.size()) {
              return false;
            }
            next = params.get(arg);
          } else {
            next = lambdaExpression.getBody();
          }
          break;
        }
        case MEMBER_REFERENCE: {
          MemberReferenceTree memberReference = (MemberReferenceTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.QUALIFIER_EXPRESSION)) {
            next = memberReference.getQualifierExpression();
          } else {
            int arg = astNode.getArgument();
            List<? extends ExpressionTree> typeArgs =
                memberReference.getTypeArguments();
            if (arg >= typeArgs.size()) {
              return false;
            }
            next = typeArgs.get(arg);
          }
          break;
        }
        case MEMBER_SELECT: {
          MemberSelectTree memberSelect = (MemberSelectTree) actualNode;
          next = memberSelect.getExpression();
          break;
        }
        case METHOD: {
          MethodTree method = (MethodTree) actualNode;
          int arg = astNode.getArgument();
          if (astNode.childSelectorIs(ASTPath.TYPE)) {
            next = method.getReturnType();
          } else if (astNode.childSelectorIs(ASTPath.PARAMETER)) {
            next = arg == -1 ? method.getReceiverParameter()
                : method.getParameters().get(arg);
          } else if (astNode.childSelectorIs(ASTPath.TYPE_PARAMETER)) {
            next = method.getTypeParameters().get(arg);
          } else if (astNode.childSelectorIs(ASTPath.BODY)) {
            next = method.getBody();
          } else {  // THROWS?
            return false;
          }
          break;
        }
        case METHOD_INVOCATION: {
          MethodInvocationTree methodInvocation =
              (MethodInvocationTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.TYPE_ARGUMENT)) {
            int arg = astNode.getArgument();
            List<? extends Tree> typeArgs = methodInvocation.getTypeArguments();
            if (arg >= typeArgs.size()) {
              return false;
            }
            next = typeArgs.get(arg);
          } else if (astNode.childSelectorIs(ASTPath.METHOD_SELECT)) {
            next = methodInvocation.getMethodSelect();
          } else {
            int arg = astNode.getArgument();
            List<? extends ExpressionTree> args = methodInvocation.getArguments();
            if (arg >= args.size()) {
              return false;
            }
            next = args.get(arg);
          }
          break;
        }
        case NEW_ARRAY: {
          NewArrayTree newArray = (NewArrayTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.TYPE)) {
            int arg = astNode.getArgument();
            if (arg < 0) {
              next = newArray.getType();
            } else {
              return arg == depth;
            }
          } else if (astNode.childSelectorIs(ASTPath.DIMENSION)) {
            int arg = astNode.getArgument();
            List<? extends ExpressionTree> dims = newArray.getDimensions();
            if (arg >= dims.size()) {
              return false;
            }
            next = dims.get(arg);
          } else {
            int arg = astNode.getArgument();
            List<? extends ExpressionTree> inits = newArray.getInitializers();
            if (arg >= inits.size()) {
              return false;
            }
            next = inits.get(arg);
          }
          break;
        }
        case NEW_CLASS: {
          NewClassTree newClass = (NewClassTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.ENCLOSING_EXPRESSION)) {
            next = newClass.getEnclosingExpression();
          } else if (astNode.childSelectorIs(ASTPath.TYPE_ARGUMENT)) {
            int arg = astNode.getArgument();
            List<? extends Tree> typeArgs = newClass.getTypeArguments();
            if (arg >= typeArgs.size()) {
              return false;
            }
            next = typeArgs.get(arg);
          } else if (astNode.childSelectorIs(ASTPath.IDENTIFIER)) {
            next = newClass.getIdentifier();
          } else if (astNode.childSelectorIs(ASTPath.ARGUMENT)) {
            int arg = astNode.getArgument();
            List<? extends ExpressionTree> args = newClass.getArguments();
            if (arg >= args.size()) {
              return false;
            }
            next = args.get(arg);
          } else {
            next = newClass.getClassBody();
          }
          break;
        }
        case PARAMETERIZED_TYPE: {
          ParameterizedTypeTree parameterizedType =
              (ParameterizedTypeTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.TYPE)) {
            next = parameterizedType.getType();
          } else {
            int arg = astNode.getArgument();
            List<? extends Tree> typeArgs = parameterizedType.getTypeArguments();
            if (arg >= typeArgs.size()) {
              return false;
            }
            next = typeArgs.get(arg);
          }
          break;
        }
        case PARENTHESIZED: {
          ParenthesizedTree parenthesized = (ParenthesizedTree) actualNode;
          next = parenthesized.getExpression();
          break;
        }
        case RETURN: {
          ReturnTree returnn = (ReturnTree) actualNode;
          next = returnn.getExpression();
          break;
        }
        case SWITCH: {
          SwitchTree zwitch = (SwitchTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = zwitch.getExpression();
          } else {
            int arg = astNode.getArgument();
            List<? extends CaseTree> cases = zwitch.getCases();
            if (arg >= cases.size()) {
              return false;
            }
            next = cases.get(arg);
          }
          break;
        }
        case SYNCHRONIZED: {
          SynchronizedTree synchronizzed = (SynchronizedTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.EXPRESSION)) {
            next = synchronizzed.getExpression();
          } else {
            next = synchronizzed.getBlock();
          }
          break;
        }
        case THROW: {
          ThrowTree throww = (ThrowTree) actualNode;
          next = throww.getExpression();
          break;
        }
        case TRY: {
          TryTree tryy = (TryTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.BLOCK)) {
            next = tryy.getBlock();
          } else if (astNode.childSelectorIs(ASTPath.CATCH)) {
            int arg = astNode.getArgument();
            List<? extends CatchTree> catches = tryy.getCatches();
            if (arg >= catches.size()) {
              return false;
            }
            next = catches.get(arg);
          } else if (astNode.childSelectorIs(ASTPath.FINALLY_BLOCK)) {
            next = tryy.getFinallyBlock();
          } else {
            int arg = astNode.getArgument();
            List<? extends Tree> resources = tryy.getResources();
            if (arg >= resources.size()) {
              return false;
            }
            next = resources.get(arg);
          }
          break;
        }
        case TYPE_CAST: {
          TypeCastTree typeCast = (TypeCastTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.TYPE)) {
            next = typeCast.getType();
          } else {
            next = typeCast.getExpression();
          }
          break;
        }
        case UNION_TYPE: {
          UnionTypeTree unionType = (UnionTypeTree) actualNode;
          int arg = astNode.getArgument();
          List<? extends Tree> typeAlts = unionType.getTypeAlternatives();
          if (arg >= typeAlts.size()) {
            return false;
          }
          next = typeAlts.get(arg);
          break;
        }
        case VARIABLE: {
          VariableTree var = (VariableTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.INITIALIZER)) {
            next = var.getInitializer();
          } else {
            next = var.getType();
          }
          break;
        }
        case WHILE_LOOP: {
          WhileLoopTree whileLoop = (WhileLoopTree) actualNode;
          if (astNode.childSelectorIs(ASTPath.CONDITION)) {
            next = whileLoop.getCondition();
          } else {
            next = whileLoop.getStatement();
          }
          break;
        }
        default: {
          if (isBinaryOperator(actualNode.getKind())) {
            BinaryTree binary = (BinaryTree) actualNode;
            if (astNode.childSelectorIs(ASTPath.LEFT_OPERAND)) {
              next = binary.getLeftOperand();
            } else {
              next = binary.getRightOperand();
            }
          } else if (isCompoundAssignment(actualNode.getKind())) {
            CompoundAssignmentTree compoundAssignment =
                (CompoundAssignmentTree) actualNode;
            if (astNode.childSelectorIs(ASTPath.VARIABLE)) {
              next = compoundAssignment.getVariable();
            } else {
              next = compoundAssignment.getExpression();
            }
          } else if (isUnaryOperator(actualNode.getKind())) {
            UnaryTree unary = (UnaryTree) actualNode;
            next = unary.getExpression();
          } else if (isWildcard(actualNode.getKind())) {
            WildcardTree wildcard = (WildcardTree) actualNode;
            // The following check is necessary because Oracle has decided that
            //   x instanceof Class<? extends Object>
            // will remain illegal even though it means the same thing as
            //   x instanceof Class<?>.
            if (i > 0) {  // TODO: refactor GenericArrayLoc to use same code?
              Tree ancestor = actualPath.get(i-1);
              if (ancestor.getKind() == Tree.Kind.INSTANCE_OF) {
                System.err.println("WARNING: wildcard bounds not allowed"
                    + " in 'instanceof' expression; skipping insertion");
                return false;
              } else if (i > 1 && ancestor.getKind() ==
                  Tree.Kind.PARAMETERIZED_TYPE) {
                ancestor = actualPath.get(i-2);
                if (ancestor.getKind() == Tree.Kind.ARRAY_TYPE) {
                  System.err.println("WARNING: wildcard bounds not allowed"
                      + " in generic array type; skipping insertion");
                  return false;
                }
              }
            }
            next = wildcard.getBound();
          } else {
            throw new IllegalArgumentException("Illegal kind: "
                + actualNode.getKind());
          }
          break;
        }
        }

        dbug.debug("next: %s%n", next);
        if (next != actualPath.get(i + 1)) {
          dbug.debug("no next match%n");
          return false;
        }
      }
      return true;
    }

    /**
     * Determines if the given kinds match, false otherwise. Two kinds match if
     * they're exactly the same or if the two kinds are both compound
     * assignments, unary operators, binary operators or wildcards.
     * <p>
     * This is necessary because in the JAIF file these kinds are represented by
     * their general types (i.e. BinaryOperator, CompoundOperator, etc.) rather
     * than their kind (i.e. PLUS, MINUS, PLUS_ASSIGNMENT, XOR_ASSIGNMENT,
     * etc.). Internally, a single kind is used to represent each general type
     * (i.e. PLUS is used for BinaryOperator, PLUS_ASSIGNMENT is used for
     * CompoundAssignment, etc.). Yet, the actual source nodes have the correct
     * kind. So if an AST path entry has a PLUS kind, that really means it could
     * be any BinaryOperator, resulting in PLUS matching any other
     * BinaryOperator.
     *
     * @param kind1
     *            the first kind to match
     * @param kind2
     *            the second kind to match
     * @return {@code true} if the kinds match as described above, {@code false}
     *         otherwise.
     */
    private static boolean kindsMatch(Tree.Kind kind1, Tree.Kind kind2) {
      return kind1 == kind2
          || (isCompoundAssignment(kind1) && isCompoundAssignment(kind2))
          || (isUnaryOperator(kind1) && isUnaryOperator(kind2))
          || (isBinaryOperator(kind1) && isBinaryOperator(kind2))
          || (isWildcard(kind1) && isWildcard(kind2));
    }

    /**
     * Determines if the given kind is a compound assignment.
     *
     * @param kind
     *            the kind to test
     * @return true if the given kind is a compound assignment
     */
    private static boolean isCompoundAssignment(Tree.Kind kind) {
      return kind == Tree.Kind.PLUS_ASSIGNMENT
          || kind == Tree.Kind.MINUS_ASSIGNMENT
          || kind == Tree.Kind.MULTIPLY_ASSIGNMENT
          || kind == Tree.Kind.DIVIDE_ASSIGNMENT
          || kind == Tree.Kind.OR_ASSIGNMENT
          || kind == Tree.Kind.AND_ASSIGNMENT
          || kind == Tree.Kind.REMAINDER_ASSIGNMENT
          || kind == Tree.Kind.LEFT_SHIFT_ASSIGNMENT
          || kind == Tree.Kind.RIGHT_SHIFT
          || kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT
          || kind == Tree.Kind.XOR_ASSIGNMENT;
    }

    /**
     * Determines if the given kind is a unary operator.
     *
     * @param kind
     *            the kind to test
     * @return true if the given kind is a unary operator
     */
    private static boolean isUnaryOperator(Tree.Kind kind) {
      return kind == Tree.Kind.POSTFIX_INCREMENT
          || kind == Tree.Kind.POSTFIX_DECREMENT
          || kind == Tree.Kind.PREFIX_INCREMENT
          || kind == Tree.Kind.PREFIX_DECREMENT
          || kind == Tree.Kind.UNARY_PLUS
          || kind == Tree.Kind.UNARY_MINUS
          || kind == Tree.Kind.BITWISE_COMPLEMENT
          || kind == Tree.Kind.LOGICAL_COMPLEMENT;
    }

    /**
     * Determines if the given kind is a binary operator.
     *
     * @param kind
     *            the kind to test
     * @return true if the given kind is a binary operator
     */
    private static boolean isBinaryOperator(Tree.Kind kind) {
      return kind == Tree.Kind.MULTIPLY
          || kind == Tree.Kind.DIVIDE
          || kind == Tree.Kind.REMAINDER
          || kind == Tree.Kind.PLUS
          || kind == Tree.Kind.MINUS
          || kind == Tree.Kind.LEFT_SHIFT
          || kind == Tree.Kind.RIGHT_SHIFT
          || kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT
          || kind == Tree.Kind.LESS_THAN
          || kind == Tree.Kind.GREATER_THAN
          || kind == Tree.Kind.LESS_THAN_EQUAL
          || kind == Tree.Kind.GREATER_THAN_EQUAL
          || kind == Tree.Kind.EQUAL_TO
          || kind == Tree.Kind.NOT_EQUAL_TO
          || kind == Tree.Kind.AND
          || kind == Tree.Kind.XOR
          || kind == Tree.Kind.OR
          || kind == Tree.Kind.CONDITIONAL_AND
          || kind == Tree.Kind.CONDITIONAL_OR;
    }

    /**
     * Determines if the given kind is a wildcard.
     *
     * @param kind
     *            the kind to test
     * @return true if the given kind is a wildcard
     */
    private static boolean isWildcard(Tree.Kind kind) {
      return kind == Tree.Kind.UNBOUNDED_WILDCARD
          || kind == Tree.Kind.EXTENDS_WILDCARD
          || kind == Tree.Kind.SUPER_WILDCARD;
    }
  }

  public static boolean isTypeSelector(String selector) {
    return Arrays.<String>binarySearch(typeSelectors,
        selector, Collator.getInstance()) >= 0;
  }

  public static boolean isClassEquiv(Tree.Kind kind) {
    switch (kind) {
      case CLASS:
      case INTERFACE:
      case ENUM:
      case ANNOTATION_TYPE:
        return true;
      default:
        return false;
    }
  }

  /**
   * Determines if the given kind is a compound assignment.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind is a compound assignment
   */
  public static boolean isCompoundAssignment(Tree.Kind kind) {
    return kind == Tree.Kind.PLUS_ASSIGNMENT
        || kind == Tree.Kind.MINUS_ASSIGNMENT
        || kind == Tree.Kind.MULTIPLY_ASSIGNMENT
        || kind == Tree.Kind.DIVIDE_ASSIGNMENT
        || kind == Tree.Kind.OR_ASSIGNMENT
        || kind == Tree.Kind.AND_ASSIGNMENT
        || kind == Tree.Kind.REMAINDER_ASSIGNMENT
        || kind == Tree.Kind.LEFT_SHIFT_ASSIGNMENT
        || kind == Tree.Kind.RIGHT_SHIFT_ASSIGNMENT
        || kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT
        || kind == Tree.Kind.XOR_ASSIGNMENT;
  }

  /**
   * Determines if the given kind is a unary operator.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind is a unary operator
   */
  public static boolean isUnaryOperator(Tree.Kind kind) {
    return kind == Tree.Kind.POSTFIX_INCREMENT
        || kind == Tree.Kind.POSTFIX_DECREMENT
        || kind == Tree.Kind.PREFIX_INCREMENT
        || kind == Tree.Kind.PREFIX_DECREMENT
        || kind == Tree.Kind.UNARY_PLUS
        || kind == Tree.Kind.UNARY_MINUS
        || kind == Tree.Kind.BITWISE_COMPLEMENT
        || kind == Tree.Kind.LOGICAL_COMPLEMENT;
  }

  /**
   * Determines if the given kind is a binary operator.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind is a binary operator
   */
  public static boolean isBinaryOperator(Tree.Kind kind) {
    return kind == Tree.Kind.MULTIPLY || kind == Tree.Kind.DIVIDE
        || kind == Tree.Kind.REMAINDER || kind == Tree.Kind.PLUS
        || kind == Tree.Kind.MINUS || kind == Tree.Kind.LEFT_SHIFT
        || kind == Tree.Kind.RIGHT_SHIFT
        || kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT
        || kind == Tree.Kind.LESS_THAN
        || kind == Tree.Kind.GREATER_THAN
        || kind == Tree.Kind.LESS_THAN_EQUAL
        || kind == Tree.Kind.GREATER_THAN_EQUAL
        || kind == Tree.Kind.EQUAL_TO || kind == Tree.Kind.NOT_EQUAL_TO
        || kind == Tree.Kind.AND || kind == Tree.Kind.XOR
        || kind == Tree.Kind.OR || kind == Tree.Kind.CONDITIONAL_AND
        || kind == Tree.Kind.CONDITIONAL_OR;
  }

  public static boolean isLiteral(Tree.Kind kind) {
    switch (kind) {
    case INT_LITERAL:
    case LONG_LITERAL:
    case FLOAT_LITERAL:
    case DOUBLE_LITERAL:
    case BOOLEAN_LITERAL:
    case CHAR_LITERAL:
    case STRING_LITERAL:
    case NULL_LITERAL:
      return true;
    default:
      return false;
    }
  }

  public static boolean isExpression(Tree.Kind kind) {
    switch (kind) {
    case ARRAY_ACCESS:
    case ASSIGNMENT:
    case CONDITIONAL_EXPRESSION:
    case EXPRESSION_STATEMENT:
    case MEMBER_SELECT:
    case MEMBER_REFERENCE:
    case IDENTIFIER:
    case INSTANCE_OF:
    case METHOD_INVOCATION:
    case NEW_ARRAY:
    case NEW_CLASS:
    case LAMBDA_EXPRESSION:
    case PARENTHESIZED:
    case TYPE_CAST:
      return true;
    default:
      return isUnaryOperator(kind) || isBinaryOperator(kind)
          || isCompoundAssignment(kind) || isLiteral(kind);
    }
  }

  public static boolean isTypeKind(Tree.Kind kind) {
    switch (kind) {
    case ANNOTATED_TYPE:
    case ARRAY_TYPE:
    case IDENTIFIER:
    case INTERSECTION_TYPE:
    // case MEMBER_SELECT:
    case PARAMETERIZED_TYPE:
    case PRIMITIVE_TYPE:
    case UNION_TYPE:
      return true;
    default:
      return false;
    }
  }

  /**
   * Determines if the given kind is a wildcard.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind is a wildcard
   */
  public static boolean isWildcard(Tree.Kind kind) {
    return kind == Tree.Kind.UNBOUNDED_WILDCARD
        || kind == Tree.Kind.EXTENDS_WILDCARD
        || kind == Tree.Kind.SUPER_WILDCARD;
  }

  /**
   * Determines if the given kind is a declaration.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind is a declaration
   */
  public static boolean isDeclaration(Tree.Kind kind) {
    return kind == Tree.Kind.ANNOTATION
        || kind == Tree.Kind.CLASS
        || kind == Tree.Kind.ENUM
        || kind == Tree.Kind.INTERFACE
        || kind == Tree.Kind.METHOD
        || kind == Tree.Kind.VARIABLE;
  }

  /**
   * Determines whether an {@code ASTPath} can identify nodes of the
   *  given kind.
   *
   * @param kind
   *            the kind to test
   * @return true if the given kind can be identified by an {@code ASTPath}.
   */
  public static boolean isHandled(Tree.Kind kind) {
    switch (kind) {
    case BREAK:
    case COMPILATION_UNIT:
    case CONTINUE:
    case IMPORT:
    case MODIFIERS:
      return false;
    default:
      return !isDeclaration(kind);
    }
  }  // TODO: need "isType"?
}

/**
 *
 * @author dbro
 *
 * @param <E> type of stack elements
 */
class ConsStack<E> implements PersistentStack<E> {
  private int size;
  private E elem;
  private PersistentStack<E> rest;

  public ConsStack() {
    size = 0;
    elem = null;
    rest = null;
  }

  private static <T, S extends ConsStack<T>> S extend(T el, S s0) {
    try {
      @SuppressWarnings("unchecked")
      S s1 = (S) s0.getClass().newInstance();
      ConsStack<T> cs = (ConsStack<T>) s1;
      cs.size = 1 + s0.size();
      cs.elem = el;
      cs.rest = s0;
      return s1;
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public boolean isEmpty() { return size == 0; }

  @Override
  public E peek() {
    if (size > 0) { return elem; }
    throw new IllegalStateException("peek() on empty stack");
  }

  @Override
  public PersistentStack<E> pop() {
    if (size > 0) { return rest; }
    throw new IllegalStateException("pop() on empty stack");
  }

  @Override
  public PersistentStack<E> push(E elem) {
    return extend(elem, this);
  }

  @Override
  public int size() { return size; }

  @Override
  public String toString() {
    if (size > 0) {
      StringBuilder sb = new StringBuilder("]").insert(0, peek());
      for (PersistentStack<E> stack = pop(); !stack.isEmpty();
          stack = stack.pop()) {
        sb = sb.insert(0, ", ").insert(0, stack.peek());
      }
      return sb.insert(0, "[").toString();
    }
    return "[]";
  }
}