Java程序  |  1701行  |  54.71 KB

package annotator.find;

import java.util.AbstractSet;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.lang.model.element.Name;
import javax.lang.model.type.TypeKind;

import annotations.el.InnerTypeLocation;
import annotations.io.ASTIndex;
import annotations.io.ASTPath;
import annotations.io.ASTRecord;
import type.*;

import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TreeVisitor;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Kinds;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntryKind;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.util.Pair;

/**
 * @author dbro
 *
 * An indexed collection (though not {@link java.util.Collection}, only
 * {@link java.lang.Iterable}) of {@link Insertion}s with methods to
 * select those specified for a given class or for an outer class along
 * with its local classes.  This class is especially useful when a
 * single JAIF stores annotations for many source files, as it reduces
 * the number of insertions to be considered for any AST node.
 *
 * The class now serves a second purpose, which should probably be
 * separated out (according to OO dogma, at least): It attaches
 * {@link annotations.io.ASTPath}-based inner type {@link Insertion}s to
 * a {@link TypedInsertion} on the outer type if one exists (see
 * {@link #organizeTypedInsertions(CompilationUnitTree, String, Collection)}.
 * Since getting these insertions right depends on this organization,
 * this class is now essential for correctness, not merely for
 * performance.
 */
public class Insertions implements Iterable<Insertion> {
  private static int kindLevel(Insertion i) {
    // ordered so insertion that depends on another gets inserted after other
    switch (i.getKind()) {
    case CONSTRUCTOR:
      return 3;
    case NEW:
    case RECEIVER:
      return 2;
    case CAST:
        return 1;
    // case ANNOTATION:
    // case CLOSE_PARENTHESIS:
    default:
      return 0;
    }
  }

  private static final Comparator<Insertion> byASTRecord =
      new Comparator<Insertion>() {
        @Override
        public int compare(Insertion o1, Insertion o2) {
          Criteria c1 = o1.getCriteria();
          Criteria c2 = o2.getCriteria();
          ASTPath p1 = c1.getASTPath();
          ASTPath p2 = c2.getASTPath();
          ASTRecord r1 = new ASTRecord(null,
              c1.getClassName(), c1.getMethodName(), c1.getFieldName(),
              p1 == null ? ASTPath.empty() : p1);
          ASTRecord r2 = new ASTRecord(null,
              c2.getClassName(), c2.getMethodName(), c2.getFieldName(),
              p2 == null ? ASTPath.empty() : p2);
          int c = r1.compareTo(r2);
          if (c == 0) {
            // c = o1.getKind().compareTo(o2.getKind());
            c = Integer.compare(kindLevel(o2), kindLevel(o1));  // descending
            if (c == 0) { c = o1.toString().compareTo(o2.toString()); }
          }
          return c;
        }
      };

  // store indexes insertions by (qualified) outer class name and inner
  // class path (if any)
  private Map<String, Map<String, Set<Insertion>>> store;
  private int size;

  private Pair<String, String> nameSplit(String name) {
    int i = name.indexOf('$');  // FIXME: don't split on '$' in source
    return i < 0
        ? Pair.of(name, "")
        : Pair.of(name.substring(0, i), name.substring(i));
  }

  public Insertions() {
    store = new HashMap<String, Map<String, Set<Insertion>>>();
    size = 0;
  }

  // auxiliary for following two methods
  private void forClass(CompilationUnitTree cut,
      String qualifiedClassName, Set<Insertion> result) {
    Pair<String, String> pair = nameSplit(qualifiedClassName);
    Map<String, Set<Insertion>> map = store.get(pair.fst);
    if (map != null) {
      Set<Insertion> set = new TreeSet<Insertion>(byASTRecord);
      set.addAll(map.get(pair.snd));
      if (set != null) {
        set = organizeTypedInsertions(cut, qualifiedClassName, set);
        result.addAll(set);
      }
    }
  }

  /**
   * Selects {@link Insertion}s relevant to a given class.
   *
   * @param cut the current compilation unit
   * @param qualifiedClassName the fully qualified class name
   * @return {@link java.util.Set} of {@link Insertion}s with an
   *          {@link InClassCriterion} for the given class
   */
  public Set<Insertion> forClass(CompilationUnitTree cut,
      String qualifiedClassName) {
    Set<Insertion> set = new LinkedHashSet<Insertion>();
    forClass(cut, qualifiedClassName, set);
    return set;
  }

  /**
   * Selects {@link Insertion}s relevant to a given outer class and its
   * local classes.
   *
   * @param cut the current compilation unit
   * @param qualifiedOuterClassName the fully qualified outer class name
   * @return {@link java.util.Set} of {@link Insertion}s with an
   *          {@link InClassCriterion} for the given outer class or one
   *          of its local classes
   */
  public Set<Insertion> forOuterClass(CompilationUnitTree cut,
      String qualifiedOuterClassName) {
    Map<String, Set<Insertion>> map = store.get(qualifiedOuterClassName);
    if (map == null || map.isEmpty()) {
      return Collections.<Insertion>emptySet();
    } else {
      Set<Insertion> set = new LinkedHashSet<Insertion>();
      for (String key : map.keySet()) {
        String qualifiedClassName = qualifiedOuterClassName + key;
        forClass(cut, qualifiedClassName, set);
      }
      return set;
    }
  }

  /**
   * Add an {@link Insertion} to this collection.
   */
  public void add(Insertion ins) {
    InClassCriterion icc = ins.getCriteria().getInClass();
    String k1 = "";
    String k2 = "";
    Map<String, Set<Insertion>> map;
    Set<Insertion> set;

    if (icc != null) {
      Pair<String, String> triple = nameSplit(icc.className);
      k1 = triple.fst;
      k2 = triple.snd;
    }
    map = store.get(k1);
    if (map == null) {
      map = new HashMap<String, Set<Insertion>>();
      store.put(k1, map);
    }
    set = map.get(k2);
    if (set == null) {
      set = new LinkedHashSet<Insertion>();
      map.put(k2, set);
    }

    size -= set.size();
    set.add(ins);
    size += set.size();
  }

  /**
   * Add all {@link Insertion}s in the given
   * {@link java.util.Collection} to this collection.
   */
  public void addAll(Collection<? extends Insertion> c) {
    for (Insertion ins : c) {
      add(ins);
    }
  }

  /**
   * Returns the number of {@link Insertion}s in this collection.
   */
  public int size() {
    return size;
  }

  @Override
  public Iterator<Insertion> iterator() {
    return new Iterator<Insertion>() {
      private Iterator<Map<String, Set<Insertion>>> miter =
          store.values().iterator();
      private Iterator<Set<Insertion>> siter =
          Collections.<Set<Insertion>>emptySet().iterator();
      private Iterator<Insertion> iiter =
          Collections.<Insertion>emptySet().iterator();

      @Override
      public boolean hasNext() {
        if (iiter.hasNext()) { return true; }
        while (siter.hasNext()) {
          iiter = siter.next().iterator();
          if (iiter.hasNext()) { return true; }
        }
        while (miter.hasNext()) {
          siter = miter.next().values().iterator();
          while (siter.hasNext()) {
            iiter = siter.next().iterator();
            if (iiter.hasNext()) { return true; }
          }
        }
        return false;
      }

      @Override
      public Insertion next() {
        if (hasNext()) { return iiter.next(); }
        throw new NoSuchElementException();
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  /**
   * Returns a {@link java.util.List} containing all {@link Insertion}s
   * in this collection.
   */
  public List<Insertion> toList() {
    List<Insertion> list = new ArrayList<Insertion>(size);
    for (Insertion ins : this) { list.add(ins); }
    return null;
  }

  /*
   * This method detects inner type relationships among ASTPath-based
   * insertion specifications and organizes the insertions accordingly.
   * This step is necessary because 1) insertion proceeds from the end to
   * the beginning of the source and 2) the insertion location does not
   * always exist prior to the top-level type insertion.
   */
  private Set<Insertion> organizeTypedInsertions(CompilationUnitTree cut,
      String className, Collection<Insertion> insertions) {
    ASTRecordMap<TypedInsertion> map = new ASTRecordMap<TypedInsertion>();
    Set<Insertion> organized = new LinkedHashSet<Insertion>();
    Set<Insertion> unorganized = new LinkedHashSet<Insertion>();
    List<Insertion> list = new ArrayList<Insertion>();

    // First divide the insertions into three buckets: TypedInsertions
    // on outer types (map), ASTPath-based insertions on local types
    // (unorganized -- built as list and then sorted, since building as
    // a set spuriously removes "duplicates" according to the
    // comparator), and everything else (organized -- where all
    // eventually land).
    for (Insertion ins : insertions) {
      if (ins.getInserted()) { continue; }
      Criteria criteria = ins.getCriteria();
      GenericArrayLocationCriterion galc =
          criteria.getGenericArrayLocation();
      ASTPath p = criteria.getASTPath();
      if (p == null || p.isEmpty()
          || galc != null && !galc.getLocation().isEmpty()
          || ins instanceof CastInsertion
          || ins instanceof CloseParenthesisInsertion) {
        organized.add(ins);
      } else {
        ASTRecord rec = new ASTRecord(cut, criteria.getClassName(),
            criteria.getMethodName(), criteria.getFieldName(), p);
        ASTPath.ASTEntry entry = rec.astPath.get(-1);
        Tree node;

        if (entry.getTreeKind() == Tree.Kind.NEW_ARRAY
            && entry.childSelectorIs(ASTPath.TYPE)
            && entry.getArgument() == 0) {
          ASTPath temp = rec.astPath.getParentPath();
          node = ASTIndex.getNode(cut, rec.replacePath(temp));
          node = node instanceof JCTree.JCNewArray
              ? TypeTree.fromType(((JCTree.JCNewArray) node).type)
              : null;
        } else {
          node = ASTIndex.getNode(cut, rec);
        }

        if (ins instanceof TypedInsertion) {
          TypedInsertion tins = map.get(rec);
          if (ins instanceof NewInsertion) {
            NewInsertion nins = (NewInsertion) ins;
            if (entry.getTreeKind() == Tree.Kind.NEW_ARRAY
                && entry.childSelectorIs(ASTPath.TYPE)) {
              int a = entry.getArgument();
              List<TypePathEntry> loc0 = new ArrayList<TypePathEntry>(a);
              ASTRecord rec0 = null;
              if (a == 0) {
                rec0 = rec.replacePath(p.getParentPath());
                Tree t = ASTIndex.getNode(cut, rec0);
                if (t == null || t.toString().startsWith("{")) {
                  rec0 = null;
                } else {
                  rec = rec0;
                  rec0 = rec.extend(Tree.Kind.NEW_ARRAY,
                      ASTPath.TYPE, 0);
                }
              } else if (node != null
                  && !nins.getInnerTypeInsertions().isEmpty()) {
                if (node.getKind() == Tree.Kind.IDENTIFIER) {
                  node = ASTIndex.getNode(cut,
                      rec.replacePath(p.getParentPath()));
                }
                if ((node.getKind() == Tree.Kind.NEW_ARRAY
                    || node.getKind() == Tree.Kind.ARRAY_TYPE)
                    && !node.toString().startsWith("{")) {
                  rec = rec.replacePath(p.getParentPath());

                  Collections.fill(loc0, TypePathEntry.ARRAY);
                  // irec = rec;
                  // if (node.getKind() == Tree.Kind.NEW_ARRAY) {
                  rec0 = rec.extend(Tree.Kind.NEW_ARRAY,
                      ASTPath.TYPE, 0);
                  // }
                }
              }

              if (rec0 != null) {
                for (Insertion inner : nins.getInnerTypeInsertions()) {
                  Criteria icriteria = inner.getCriteria();
                  GenericArrayLocationCriterion igalc =
                      icriteria.getGenericArrayLocation();
                  if (igalc != null) {
                    ASTRecord rec1;
                    int b = igalc.getLocation().size();
                    List<TypePathEntry> loc =
                        new ArrayList<TypePathEntry>(a + b);
                    loc.addAll(loc0);
                    loc.addAll(igalc.getLocation());
                    rec1 = extendToInnerType(rec0, loc, node);
                    icriteria.add(new GenericArrayLocationCriterion());
                    icriteria.add(new ASTPathCriterion(rec1.astPath));
                    inner.setInserted(false);
                    organized.add(inner);
                  }
                }
                nins.getInnerTypeInsertions().clear();
              }
            }
          }
          if (tins == null) {
            map.put(rec, (TypedInsertion) ins);
          } else if (tins.getType().equals(((TypedInsertion) ins).getType())) {
            mergeTypedInsertions(tins, (TypedInsertion) ins);
          }
        } else {
          int d = newArrayInnerTypeDepth(p);
          if (d > 0) {
            ASTPath temp = p;
            while (!temp.isEmpty()
                && (node == null || node.getKind() != Tree.Kind.NEW_ARRAY)) {
              // TODO: avoid repeating work of newArrayInnerTypeDepth()
              temp = temp.getParentPath();
              node = ASTIndex.getNode(cut, rec.replacePath(temp));
            }
            if (node == null) {
              // TODO: ???
            }
            temp = temp.extend(
                new ASTPath.ASTEntry(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, 0));
            if (node.toString().startsWith("{")) {
              TypedInsertion tins = map.get(rec.replacePath(temp));
              if (tins == null) {
                // TODO
              } else {
                tins.getInnerTypeInsertions().add(ins);
                ins.setInserted(true);
              }
            } else {
              List<? extends ExpressionTree> dims =
                  ((NewArrayTree) node).getDimensions();
              ASTRecord irec = rec.replacePath(p.getParentPath())
                  .extend(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, 0);
              GenericArrayLocationCriterion igalc =
                  criteria.getGenericArrayLocation();
              for (int i = 0 ; i < d; i++) {
                irec = irec.extend(Tree.Kind.ARRAY_TYPE, ASTPath.TYPE);
              }
              if (igalc != null) {
                List<TypePathEntry> loc = igalc.getLocation();
                if (!loc.isEmpty()) {
                  try {
                    Tree dim = dims.get(d-1);
                    irec = extendToInnerType(irec, loc, dim);
                    criteria.add(new ASTPathCriterion(irec.astPath));
                    criteria.add(new GenericArrayLocationCriterion());
                  } catch (RuntimeException e) {}
                }
              }
            }
          }
          list.add(ins);
        }
      }
    }
    // if (map.isEmpty()) {
    //  organized.addAll(unorganized);
    //  return organized;
    // }
    Collections.sort(list, byASTRecord);
    unorganized.addAll(list);

    // Each Insertion in unorganized gets attached to a TypedInsertion
    // in map if possible; otherwise, it gets dumped into organized.
    for (Insertion ins : unorganized) {
      Criteria criteria = ins.getCriteria();
      String methodName = criteria.getMethodName();
      String fieldName = criteria.getFieldName();
      ASTPath ap1 = criteria.getASTPath();
      List<TypePathEntry> tpes = new ArrayList<TypePathEntry>();
      if (ap1 == null) {
          // || methodName == null && fieldName == null)
        organized.add(ins);
        continue;
      }

      // First find the relevant "top-level" insertion, if any.
      // ap0: path to top-level type; ap1: path to local type
      ASTRecord rec;
      Tree.Kind kind;
      Deque<ASTPath> astack = new ArrayDeque<ASTPath>(ap1.size());
      ASTPath ap0 = ap1;
      do {
        astack.push(ap0);
        ap0 = ap0.getParentPath();
      } while (!ap0.isEmpty());
      do {
        ap0 = astack.pop();
        kind = ap0.get(-1).getTreeKind();
        rec = new ASTRecord(cut, className, methodName, fieldName, ap0);
      } while (!(astack.isEmpty() || map.containsKey(rec)));

      TypedInsertion tins = map.get(rec);
      TreePath path = ASTIndex.getTreePath(cut, rec);
      Tree node = path == null ? null : path.getLeaf();
      if (node == null && ap0.isEmpty()) {
        organized.add(ins);
        continue;
      }

      // Try to create a top-level insertion if none exists (e.g., if
      // there is an insertion for NewArray.type 1 but not for 0).
      if (tins == null) {
        GenericArrayLocationCriterion galc =
            criteria.getGenericArrayLocation();
        if (node == null) {
          // TODO: figure out from rec?
          organized.add(ins);
          continue;
        } else {
          Tree t = path.getLeaf();
          switch (t.getKind()) {
          case NEW_ARRAY:
            int d = 0;
            ASTPath.ASTEntry e = ap1.get(-1);
            List<TypePathEntry> loc = null;
            List<Insertion> inners = new ArrayList<Insertion>();
            Type type = TypeTree.conv(((JCTree.JCNewArray) t).type);
            if (e.getTreeKind() == Tree.Kind.NEW_ARRAY) {
              d += e.getArgument();
            }
            if (galc != null) {
              loc = galc.getLocation();
              int n = loc.size();
              while (--n >= 0 && loc.get(n).tag == TypePathEntryKind.ARRAY) {
                ++d;
              }
              loc = n < 0 ? null : loc.subList(0, ++n);
            }
            criteria.add(new ASTPathCriterion(
                rec.astPath.getParentPath().extendNewArray(d)));
            criteria.add(loc == null || loc.isEmpty()
                ? new GenericArrayLocationCriterion()
                : new GenericArrayLocationCriterion(
                    new InnerTypeLocation(loc)));
            inners.add(ins);
            tins = new NewInsertion(type, criteria, inners);
            tins.setInserted(true);
            map.put(rec, tins);
            break;
          default:
            break;
          }
          path = path.getParentPath();
        }
      }

      // The sought node may or may not be found in the tree; if not, it
      // may need to be created later.  Use whatever part of the path
      // exists already to distinguish MEMBER_SELECT nodes that indicate
      // qualifiers from those that indicate local types.  Assume any
      // MEMBER_SELECTs in the AST path that don't correspond to
      // existing nodes are part of a type use.
      if (node == null) {
        ASTPath ap = ap0;
        if (!ap.isEmpty()) {
          do {
            ap = ap.getParentPath();
            node = ASTIndex.getNode(cut, rec.replacePath(ap));
          } while (node == null && !ap.isEmpty());
        }
        if (node == null) {
          organized.add(ins);
          continue;
        }

        // find actual type
        ClassSymbol csym = null;
        switch (tins.getKind()) {
        case CONSTRUCTOR:
          if (node instanceof JCTree.JCMethodDecl) {
            MethodSymbol msym = ((JCTree.JCMethodDecl) node).sym;
            csym = (ClassSymbol) msym.owner;
            node = TypeTree.fromType(csym.type);
            break;
          } else if (node instanceof JCTree.JCClassDecl) {
            csym = ((JCTree.JCClassDecl) node).sym;
            if (csym.owner instanceof ClassSymbol) {
              csym = (ClassSymbol) csym.owner;
              node = TypeTree.fromType(csym.type);
              break;
            }
          }
          throw new RuntimeException();
        case NEW:
          if (node instanceof JCTree.JCNewArray) {
            if (node.toString().startsWith("{")) {
              node = TypeTree.fromType(((JCTree.JCNewArray) node).type);
              break;
            } else {
              organized.add(ins);
              continue;
            }
          }
          throw new RuntimeException();
        case RECEIVER:
          if (node instanceof JCTree.JCMethodDecl) {
            JCTree.JCMethodDecl jmd = (JCTree.JCMethodDecl) node;
            csym = (ClassSymbol) jmd.sym.owner;
            if ("<init>".equals(jmd.name.toString())) {
              csym = (ClassSymbol) csym.owner;
            }
          } else if (node instanceof JCTree.JCClassDecl) {
            csym = ((JCTree.JCClassDecl) node).sym;
          }
          if (csym != null) {
            node = TypeTree.fromType(csym.type);
            break;
          }
          throw new RuntimeException();
        default:
          throw new RuntimeException();
        }
      }

      /*
       * Inner types require special consideration due to the
       * structural differences between an AST that represents a type
       * (subclass of com.sun.source.Tree) and the type's logical
       * representation (subclass of type.Type).  The differences are
       * most prominent in the case of a type with a parameterized
       * local type.  For example, the AST for A.B.C<D> looks like
       * this:
       *
       *                     ParameterizedType
       *                    /                 \
       *               MemberSelect       Identifier
       *              /            \           |
       *        MemberSelect      (Name)       D
       *         /      \           |
       *  Identifier   (Name)       C
       *        |        |
       *        A        B
       *
       * (Technically, the Names are not AST nodes but rather
       * attributes of their parent MemberSelect nodes.)  The logical
       * representation seems more intuitive:
       *
       *       DeclaredType
       *      /     |      \
       *    Name  Params  Inner
       *     |      |       |
       *     A      -  DeclaredType
       *              /     |      \
       *            Name  Params  Inner
       *             |      |       |
       *             B      -  DeclaredType
       *                      /     |      \
       *                    Name  Params  Inner
       *                     |      |       |
       *                     C      D       -
       *
       * The opposing "chirality" of local type nesting means that the
       * usual recursive descent strategy doesn't work for finding a
       * logical type path in an AST; in effect, local types have to
       * be "turned inside-out".
       *
       * Worse yet, the actual tree structure may not exist in the tree!
       * It is possible to recover the actual type from the symbol
       * table, but the methods to create AST nodes are not visible
       * here.  Hence, the conversion relies on custom implementations
       * of the interfaces in com.sun.source.tree.Tree, which are
       * defined in the local class TypeTree.
       */
      int i = ap0.size();
      int n = ap1.size();
      int actualDepth = 0;  // inner type levels seen
      int expectedDepth = 0;  // inner type levels anticipated

      // skip any declaration nodes
      while (i < n) {
        ASTPath.ASTEntry entry = ap1.get(i);
        kind = entry.getTreeKind();
        if (kind != Tree.Kind.METHOD && kind != Tree.Kind.VARIABLE) {
          break;
        }
        ++i;
      }

      // now build up the type path in JVM's format
      while (i < n) {
        ASTPath.ASTEntry entry = ap1.get(i);
        rec = rec.extend(entry);
        kind = entry.getTreeKind();

        while (node.getKind() == Tree.Kind.ANNOTATED_TYPE) {  // skip
          node = ((AnnotatedTypeTree) node).getUnderlyingType();
        }
        if (expectedDepth == 0) {
          expectedDepth = localDepth(node);
        }

        switch (kind) {
        case ARRAY_TYPE:
          if (expectedDepth == 0 && node.getKind() == kind) {
            node = ((ArrayTypeTree) node).getType();
            while (--actualDepth >= 0) {
              tpes.add(TypePathEntry.INNER_TYPE);
            }
            tpes.add(TypePathEntry.ARRAY);
            break;
          }
          throw new RuntimeException();

        case MEMBER_SELECT:
          if (--expectedDepth >= 0) {  // otherwise, shouldn't have MEMBER_SELECT
            node = ((MemberSelectTree) node).getExpression();
            ++actualDepth;
            break;
          }
          throw new RuntimeException();

        case NEW_ARRAY:
          assert tpes.isEmpty();
          ap0 = ap0.add(new ASTPath.ASTEntry(Tree.Kind.NEW_ARRAY,
              ASTPath.TYPE, 0));
          if (expectedDepth == 0 && node.getKind() == kind) {
            if (node instanceof JCTree.JCNewArray) {
              int arg = entry.getArgument();
              if (arg > 0) {
                node = ((JCTree.JCNewArray) node).elemtype;
                tpes.add(TypePathEntry.ARRAY);
                while (--arg > 0 && node instanceof JCTree.JCArrayTypeTree) {
                  node = ((JCTree.JCArrayTypeTree) node).elemtype;
                  tpes.add(TypePathEntry.ARRAY);
                }
                if (arg > 0) { throw new RuntimeException(); }
              } else {
                node = TypeTree.fromType(((JCTree.JCNewArray) node).type);
              }
            } else {
              throw new RuntimeException("NYI");  // TODO
            }
            break;
          }
          throw new RuntimeException();

        case PARAMETERIZED_TYPE:
          if (node.getKind() == kind) {
            ParameterizedTypeTree ptt = (ParameterizedTypeTree) node;
            if (entry.childSelectorIs(ASTPath.TYPE)) {
              node = ptt.getType();
              break;  // ParameterizedType.type is "transparent" wrt type path
            } else if (expectedDepth == 0
                && entry.childSelectorIs(ASTPath.TYPE_ARGUMENT)) {
              List<? extends Tree> typeArgs = ptt.getTypeArguments();
              int j = entry.getArgument();
              if (j >= 0 && j < typeArgs.size()) {
                // make sure any inner types are accounted for
                actualDepth = 0;
                expectedDepth = localDepth(ptt.getType());
                while (--expectedDepth >= 0) {
                  tpes.add(TypePathEntry.INNER_TYPE);
                }
                node = typeArgs.get(j);
                tpes.add(
                    new TypePathEntry(TypePathEntryKind.TYPE_ARGUMENT, j));
                break;
              }
            }
          }
          throw new RuntimeException();

        case UNBOUNDED_WILDCARD:
          if (ASTPath.isWildcard(node.getKind())) {
            if (expectedDepth == 0
                && (i < 1
                    || ap1.get(i-1).getTreeKind() != Tree.Kind.INSTANCE_OF)
                && (i < 2
                    || ap1.get(i-2).getTreeKind() != Tree.Kind.ARRAY_TYPE)) {
              while (--actualDepth >= 0) {
                tpes.add(TypePathEntry.INNER_TYPE);
              }
              tpes.add(TypePathEntry.WILDCARD);
              break;
            }
          }
          throw new RuntimeException();

        default:
          node = ASTIndex.getNode(cut, rec);
          break;
        }

        ++i;
      }

      while (--actualDepth >= 0) {
        tpes.add(TypePathEntry.INNER_TYPE);
      }

      organized.add(ins);
      if (tpes.isEmpty()) {
        // assert ap1.equals(ap0) && !map.containsKey(ap0);
//        organized.add(ins);
        // map.put(rec, (TypedInsertion) ins);
      } else {
        criteria.add(new ASTPathCriterion(ap0));
        criteria.add(new GenericArrayLocationCriterion(
            new InnerTypeLocation(tpes)));
        tins.getInnerTypeInsertions().add(ins);
      }
    }
    organized.addAll(map.values());
    return organized;
  }

  private int newArrayInnerTypeDepth(ASTPath path) {
    int d = 0;
    if (path != null) {
      while (!path.isEmpty()) {
        ASTPath.ASTEntry entry = path.get(-1);
        switch (entry.getTreeKind()) {
        case ANNOTATED_TYPE:
        case MEMBER_SELECT:
        case PARAMETERIZED_TYPE:
        case UNBOUNDED_WILDCARD:
          d = 0;
          break;
        case ARRAY_TYPE:
          ++d;
          break;
        case NEW_ARRAY:
          if (entry.childSelectorIs(ASTPath.TYPE) && entry.hasArgument()) {
            d += entry.getArgument();
          }
          return d;
        default:
          return 0;
        }
        path = path.getParentPath();
      }
    }
    return 0;
  }

  /**
   * Find an {@link ASTRecord} for the tree corresponding to a nested
   * type of the type (use) to which the given record corresponds.
   *
   * @param rec record of (outer) type AST to be annotated
   * @param loc inner type path
   * @return record that locates the (nested) type in the source
   */
  private ASTRecord extendToInnerType(ASTRecord rec, List<TypePathEntry> loc) {
    ASTRecord r = rec;
    Iterator<TypePathEntry> iter = loc.iterator();
    int depth = 0;

    while (iter.hasNext()) {
      TypePathEntry tpe = iter.next();
      switch (tpe.tag) {
      case ARRAY:
        while (depth-- > 0) {
          r = r.extend(Tree.Kind.MEMBER_SELECT, ASTPath.EXPRESSION);
        }
        r = r.extend(Tree.Kind.ARRAY_TYPE, ASTPath.TYPE);
        break;
      case INNER_TYPE:
        ++depth;
        break;
      case TYPE_ARGUMENT:
        depth = 0;
        r = r.extend(Tree.Kind.PARAMETERIZED_TYPE, ASTPath.TYPE_ARGUMENT,
            tpe.arg);
        break;
      case WILDCARD:
        while (depth-- > 0) {
          r = r.extend(Tree.Kind.MEMBER_SELECT, ASTPath.EXPRESSION);
        }
        r = r.extend(Tree.Kind.UNBOUNDED_WILDCARD, ASTPath.BOUND);
        break;
      default:
        throw new RuntimeException();
      }
    }
    while (depth-- > 0) {
      r = r.extend(Tree.Kind.MEMBER_SELECT, ASTPath.EXPRESSION);
    }
    return r;
  }

  /**
   * Find an {@link ASTRecord} for the tree corresponding to a nested
   * type of the type (use) to which the given tree and record
   * correspond.
   *
   * @param rec record that locates {@code node} in the source
   * @param loc inner type path
   * @param node starting point for inner type path
   * @return record that locates the nested type in the source
   */
  private ASTRecord extendToInnerType(ASTRecord rec,
      List<TypePathEntry> loc, Tree node) {
    ASTRecord r = rec;
    Tree t = node;
    Iterator<TypePathEntry> iter = loc.iterator();
    TypePathEntry tpe = iter.next();

outer:
    while (true) {
      int d = localDepth(node);

      switch (t.getKind()) {
      case ANNOTATED_TYPE:
        r = r.extend(Tree.Kind.ANNOTATED_TYPE, ASTPath.TYPE);
        t = ((JCTree.JCAnnotatedType) t).getUnderlyingType();
        break;

      case ARRAY_TYPE:
        if (d == 0 && tpe.tag == TypePathEntryKind.ARRAY) {
          int a = 0;
          if (!r.astPath.isEmpty()) {
            ASTPath.ASTEntry e = r.astPath.get(-1);
            if (e.getTreeKind() == Tree.Kind.NEW_ARRAY
                && e.childSelectorIs(ASTPath.TYPE)) {
              a = 1 + e.getArgument();
            }
          }
          r = a > 0
              ? r.replacePath(r.astPath.getParentPath())
                  .extend(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, a)
              : r.extend(Tree.Kind.ARRAY_TYPE, ASTPath.TYPE);
          t = ((ArrayTypeTree) t).getType();
          break;
        }
        throw new RuntimeException();

      case MEMBER_SELECT:
        if (d > 0 && tpe.tag == TypePathEntryKind.INNER_TYPE) {
          Tree temp = t;
          do {
            temp = ((JCTree.JCFieldAccess) temp).getExpression();
            if (!iter.hasNext()) {
              do {
                r = r.extend(Tree.Kind.MEMBER_SELECT, ASTPath.EXPRESSION);
              } while (--d > 0);
              return r;
            }
            tpe = iter.next();
            if (--d == 0) {
              continue outer;  // avoid next() at end of loop
            }
          } while (tpe.tag == TypePathEntryKind.INNER_TYPE);
        }
        throw new RuntimeException();

      case NEW_ARRAY:
        if (d == 0) {
          if (!r.astPath.isEmpty()) {
            ASTPath.ASTEntry e = r.astPath.get(-1);
            if (e.getTreeKind() == Tree.Kind.NEW_ARRAY) {
              int a = 0;
              while (tpe.tag == TypePathEntryKind.ARRAY) {
                ++a;
                if (!iter.hasNext()) { break; }
                tpe = iter.next();
              }
              r = r.replacePath(r.astPath.getParentPath())
                  .extend(Tree.Kind.NEW_ARRAY, ASTPath.TYPE, a);
              break;
            }
          }
          r = r.extend(Tree.Kind.ARRAY_TYPE, ASTPath.TYPE);
          t = ((JCTree.JCArrayTypeTree) t).getType();
          break;
        }
        throw new RuntimeException();

      case PARAMETERIZED_TYPE:
        if (d == 0 && tpe.tag == TypePathEntryKind.TYPE_ARGUMENT) {
          r = r.extend(Tree.Kind.PARAMETERIZED_TYPE,
              ASTPath.TYPE_ARGUMENT, tpe.arg);
          t = ((JCTree.JCTypeApply) t).getTypeArguments().get(tpe.arg);
          break;
        } else if (d > 0 && tpe.tag == TypePathEntryKind.INNER_TYPE) {
          Tree temp = ((JCTree.JCTypeApply) t).getType();
          r = r.extend(Tree.Kind.PARAMETERIZED_TYPE, ASTPath.TYPE);
          t = temp;
          do {
            temp = ((JCTree.JCFieldAccess) temp).getExpression();
            if (!iter.hasNext()) {
              do {
                r = r.extend(Tree.Kind.MEMBER_SELECT, ASTPath.EXPRESSION);
              } while (--d > 0);
              return r;
            }
            tpe = iter.next();
            if (--d == 0) {
              continue outer;  // avoid next() at end of loop
            }
          } while (tpe.tag == TypePathEntryKind.INNER_TYPE);
        }
        throw new RuntimeException();

      case EXTENDS_WILDCARD:
      case SUPER_WILDCARD:
      case UNBOUNDED_WILDCARD:
        if (tpe.tag == TypePathEntryKind.WILDCARD) {
          t = ((JCTree.JCWildcard) t).getBound();
          break;
        }
        throw new RuntimeException();

      default:
        if (iter.hasNext()) {
          throw new RuntimeException();
        }
      }

      if (!iter.hasNext()) { return r; }
      tpe = iter.next();
    }
  }

  // merge annotations, assuming types are structurally identical
  private void mergeTypedInsertions(TypedInsertion ins0, TypedInsertion ins1) {
    mergeTypes(ins0.getType(), ins1.getType());
  }

  private void mergeTypes(Type t0, Type t1) {
    if (t0 == t1) { return; }
    switch (t0.getKind()) {
    case ARRAY:
      {
        ArrayType at0 = (ArrayType) t0;
        ArrayType at1 = (ArrayType) t1;
        mergeTypes(at0.getComponentType(), at1.getComponentType());
        return;
      }
    case BOUNDED:
      {
        BoundedType bt0 = (BoundedType) t0;
        BoundedType bt1 = (BoundedType) t1;
        if (bt0.getBoundKind() != bt1.getBoundKind()) { break; }
        mergeTypes(bt0.getBound(), bt1.getBound());
        mergeTypes(bt0.getName(), bt1.getName());
        return;
      }
    case DECLARED:
      {
        DeclaredType dt0 = (DeclaredType) t0;
        DeclaredType dt1 = (DeclaredType) t1;
        List<Type> tps0 = dt0.getTypeParameters();
        List<Type> tps1 = dt1.getTypeParameters();
        int n = tps0.size();
        if (tps1.size() != n) { break; }
        mergeTypes(dt0.getInnerType(), dt1.getInnerType());
        for (String anno : dt1.getAnnotations()) {
          if (!dt0.getAnnotations().contains(anno)) {
            dt0.addAnnotation(anno);
          }
        }
        for (int i = 0; i < n; i++) {
          mergeTypes(tps0.get(i), tps1.get(i));
        }
        return;
      }
    }
    throw new RuntimeException();
  }

  // Returns the depth of the innermost local type of a type AST.
  private int localDepth(Tree node) {
    Tree t = node;
    int n = 0;
loop:
    while (t != null) {
      switch (t.getKind()) {
      case ANNOTATED_TYPE:
        t = ((AnnotatedTypeTree) t).getUnderlyingType();
        break;
      case MEMBER_SELECT:
        if (t instanceof JCTree.JCFieldAccess) {
          JCTree.JCFieldAccess jfa = (JCTree.JCFieldAccess) t;
          if (jfa.sym.kind == Kinds.PCK) {
            t = jfa.getExpression();
            continue;
          }
        }
        t = ((MemberSelectTree) t).getExpression();
        ++n;
        break;
      default:
        break loop;
      }
    }
    return n;
  }

  // Provides an additional level of indexing.
  class ASTRecordMap<E> implements Map<ASTRecord, E> {
    Map<ASTRecord, SortedMap<ASTPath, E>> back;

    ASTRecordMap() {
      back = new HashMap<ASTRecord, SortedMap<ASTPath, E>>();
    }

    private SortedMap<ASTPath, E> getMap(ASTRecord rec) {
      ASTRecord key = rec.replacePath(ASTPath.empty());
      SortedMap<ASTPath, E> map = back.get(key);
      if (map == null) {
        map = new TreeMap<ASTPath, E>();
        back.put(key, map);
      }
      return map;
    }

    @Override
    public int size() {
      int n = 0;
      for (SortedMap<ASTPath, E> map : back.values()) {
        n += map.size();
      }
      return n;
    }

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

    @Override
    public boolean containsKey(Object key) {
      ASTRecord rec = (ASTRecord) key;
      SortedMap<ASTPath, E> m = getMap(rec);
      return m != null && m.containsKey(rec.astPath);
    }

    @Override
    public boolean containsValue(Object value) {
      @SuppressWarnings("unchecked")
      E e = (E) value;
      for (SortedMap<ASTPath, E> map : back.values()) {
        if (map.containsValue(e)) { return true; }
      }
      return false;
    }

    @Override
    public E get(Object key) {
      ASTRecord rec = (ASTRecord) key;
      SortedMap<ASTPath, E> map = getMap(rec);
      return map == null ? null : map.get(rec.astPath);
    }

    @Override
    public E put(ASTRecord key, E value) {
      ASTRecord rec = key;
      SortedMap<ASTPath, E> map = getMap(rec);
      return map == null ? null : map.put(rec.astPath, value);
    }

    @Override
    public E remove(Object key) {
      ASTRecord rec = (ASTRecord) key;
      SortedMap<ASTPath, E> map = getMap(rec);
      return map == null ? null : map.remove(rec.astPath);
    }

    @Override
    public void putAll(Map<? extends ASTRecord, ? extends E> m) {
      for (Map.Entry<? extends ASTRecord, ? extends E> entry : m.entrySet()) {
        put(entry.getKey(), entry.getValue());
      }
    }

    @Override
    public void clear() {
      back.clear();
    }

    @Override
    public Set<ASTRecord> keySet() {
      return back.keySet();
    }

    @Override
    public Collection<E> values() {
      Set<E> ret = new LinkedHashSet<E>();
      for (SortedMap<ASTPath, E> m : back.values()) {
        ret.addAll(m.values());
      }
      return ret;
    }

    @Override
    public Set<Map.Entry<ASTRecord, E>> entrySet() {
      final int size = size();
      return new AbstractSet<Map.Entry<ASTRecord, E>>() {
        @Override
        public Iterator<Map.Entry<ASTRecord, E>> iterator() {
          return new Iterator<Map.Entry<ASTRecord, E>>() {
            Iterator<Map.Entry<ASTRecord, SortedMap<ASTPath, E>>> iter0 =
                back.entrySet().iterator();
            Iterator<Map.Entry<ASTPath, E>> iter1 =
                Collections.<Map.Entry<ASTPath, E>>emptyIterator();
            ASTRecord rec = null;

            @Override
            public boolean hasNext() {
              if (iter1.hasNext()) { return true; }
              while (iter0.hasNext()) {
                Map.Entry<ASTRecord, SortedMap<ASTPath, E>> entry =
                    iter0.next();
                rec = entry.getKey();
                iter1 = entry.getValue().entrySet().iterator();
                if (iter1.hasNext()) { return true; }
              }
              iter1 = Collections.<Map.Entry<ASTPath, E>>emptyIterator();
              return false;
            }

            @Override
            public Map.Entry<ASTRecord, E> next() {
              if (!hasNext()) { throw new NoSuchElementException(); }
              final Map.Entry<ASTPath, E> e0 = iter1.next();
              return new Map.Entry<ASTRecord, E>() {
                final ASTRecord key = rec.replacePath(e0.getKey());
                final E val = e0.getValue();
                @Override public ASTRecord getKey() { return key; }
                @Override public E getValue() { return val; }
                @Override public E setValue(E value) {
                  throw new UnsupportedOperationException();
                }
              };
            }

            @Override
            public void remove() {
              throw new UnsupportedOperationException();
            }
          };
        }

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

  // Simple AST implementation used only in determining type paths.
  static abstract class TypeTree implements ExpressionTree {
    private static Map<String, TypeTag> primTags =
        new HashMap<String, TypeTag>();
    {
      primTags.put("byte", TypeTag.BYTE);
      primTags.put("char", TypeTag.CHAR);
      primTags.put("short", TypeTag.SHORT);
      primTags.put("long", TypeTag.LONG);
      primTags.put("float", TypeTag.FLOAT);
      primTags.put("int", TypeTag.INT);
      primTags.put("double", TypeTag.DOUBLE);
      primTags.put("boolean", TypeTag.BOOLEAN);
    }

    static TypeTree fromJCTree(JCTree jt) {
      if (jt != null) {
        Kind kind = jt.getKind();
        switch (kind) {
        case ANNOTATED_TYPE:
          return fromJCTree(
              ((JCTree.JCAnnotatedType) jt).getUnderlyingType());
        case IDENTIFIER:
          return new IdenT(
              ((JCTree.JCIdent) jt).sym.getSimpleName().toString());
        case ARRAY_TYPE:
          return new ArrT(
              fromJCTree(((JCTree.JCArrayTypeTree) jt).getType()));
        case MEMBER_SELECT:
          return new LocT(
              fromJCTree(((JCTree.JCFieldAccess) jt).getExpression()),
              ((JCTree.JCFieldAccess) jt).getIdentifier());
        case EXTENDS_WILDCARD:
        case SUPER_WILDCARD:
          return new WildT(kind,
              fromJCTree(((JCTree.JCWildcard) jt).getBound()));
        case UNBOUNDED_WILDCARD:
          return new WildT();
        case PARAMETERIZED_TYPE:
          com.sun.tools.javac.util.List<JCExpression> typeArgs =
            ((JCTree.JCTypeApply) jt).getTypeArguments();
          List<Tree> args = new ArrayList<Tree>(typeArgs.size());
          for (JCTree.JCExpression typeArg : typeArgs) {
            args.add(fromJCTree(typeArg));
          }
          return new ParT(
              fromJCTree(((JCTree.JCTypeApply) jt).getType()),
              args);
        default:
          break;
        }
      }
      return null;
    }

    static TypeTree fromType(final Type type) {
      switch (type.getKind()) {
      case ARRAY:
        final ArrayType atype = (ArrayType) type;
        final TypeTree componentType = fromType(atype.getComponentType());
        return new ArrT(componentType);
      case BOUNDED:
        final BoundedType btype = (BoundedType) type;
        final BoundedType.BoundKind bk = btype.getBoundKind();
        final String bname = btype.getName().getName();
        final TypeTree bound = fromType(btype.getBound());
        return new Param(bname, bk, bound);
      case DECLARED:
        final DeclaredType dtype = (DeclaredType) type;
        if (dtype.isWildcard()) {
          return new WildT();
        } else {
          final String dname = dtype.getName();
          TypeTag typeTag = primTags.get(dname);
          if (typeTag == null) {
            final TypeTree base = new IdenT(dname);
            TypeTree ret = base;
            List<Type> params = dtype.getTypeParameters();
            DeclaredType inner = dtype.getInnerType();
            if (!params.isEmpty()) {
              final List<Tree> typeArgs = new ArrayList<Tree>(params.size());
              for (Type t : params) { typeArgs.add(fromType(t)); }
              ret = new ParT(base, typeArgs);
            }
            return inner == null ? ret : meld(fromType(inner), ret);
          } else {
            final TypeKind typeKind = typeTag.getPrimitiveTypeKind();
            return new PrimT(typeKind);
          }
        }
      default:
        throw new RuntimeException("unknown type kind " + type.getKind());
      }
    }

    static TypeTree fromType(final com.sun.tools.javac.code.Type type) {
      return fromType(conv(type));
    }

    /**
     * @param jtype
     * @return
     */
    static Type conv(final com.sun.tools.javac.code.Type jtype) {
      Type type = null;
      DeclaredType d;
      com.sun.tools.javac.code.Type t;
      switch (jtype.getKind()) {
      case ARRAY:
        t = ((com.sun.tools.javac.code.Type.ArrayType) jtype).elemtype;
        type = new ArrayType(conv(t));
        break;
      case DECLARED:
        t = jtype;
        d = null;
        do {
          DeclaredType d0 = d;
          com.sun.tools.javac.code.Type.ClassType ct =
              (com.sun.tools.javac.code.Type.ClassType) t;
          d = new DeclaredType(ct.tsym.name.toString());
          d.setInnerType(d0);
          d0 = d;
          for (com.sun.tools.javac.code.Type a : ct.getTypeArguments()) {
            d.addTypeParameter(conv(a));
          }
          t = ct.getEnclosingType();
        } while (t.getKind() == TypeKind.DECLARED);
        type = d;
        break;
      case WILDCARD:
        BoundedType.BoundKind k;
        t = ((com.sun.tools.javac.code.Type.WildcardType) jtype).bound;
        switch (((com.sun.tools.javac.code.Type.WildcardType) jtype).kind) {
        case EXTENDS:
          k = BoundedType.BoundKind.EXTENDS;
          break;
        case SUPER:
          k = BoundedType.BoundKind.SUPER;
          break;
        case UNBOUND:
          k = null;
          type = new DeclaredType("?");
          break;
        default:
          throw new RuntimeException();
        }
        if (k != null) {
          d = new DeclaredType(jtype.tsym.name.toString());
          type = new BoundedType(d, k, (DeclaredType) conv(t));
        }
        break;
      case TYPEVAR:
        t = ((com.sun.tools.javac.code.Type.TypeVar) jtype).getUpperBound();
        type = conv(t);
        if (type.getKind() == Type.Kind.DECLARED) {
          type = new BoundedType(new DeclaredType(jtype.tsym.name.toString()),
              BoundedType.BoundKind.EXTENDS, (DeclaredType) type);
        }  // otherwise previous conv should have been here already
        break;
      case INTERSECTION:
        t = jtype.tsym.erasure_field;  // ???
        type = new DeclaredType(t.tsym.name.toString());
        break;
      case UNION:
        // TODO
        break;
      case BOOLEAN:
      case BYTE:
      case CHAR:
      case DOUBLE:
      case LONG:
      case SHORT:
      case FLOAT:
      case INT:
        type = new DeclaredType(jtype.tsym.name.toString());
        break;
      // case ERROR:
      // case EXECUTABLE:
      // case NONE:
      // case NULL:
      // case OTHER:
      // case PACKAGE:
      // case VOID:
      default:
        break;
      }
      return type;
    }

    private static TypeTree meld(final TypeTree t0, final TypeTree t1) {
      switch (t0.getKind()) {
      case IDENTIFIER:
        IdenT it = (IdenT) t0;
        return new LocT(t1, it.getName());
      case MEMBER_SELECT:
        LocT lt = (LocT) t0;
        return new LocT(meld(lt.getExpression(), t1), lt.getIdentifier());
      case PARAMETERIZED_TYPE:
        ParT pt = (ParT) t0;
        return new ParT(meld(pt.getType(), t1), pt.getTypeArguments());
      default:
        throw new IllegalArgumentException("unexpected type " + t0);
      }
    }

    static final class ArrT extends TypeTree implements ArrayTypeTree {
      private final TypeTree componentType;

      ArrT(TypeTree componentType) {
        this.componentType = componentType;
      }

      @Override
      public Kind getKind() { return Kind.ARRAY_TYPE; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitArrayType(this, data);
      }

      @Override
      public TypeTree getType() { return componentType; }

      @Override
      public String toString() { return componentType + "[]"; }
    }

    static final class LocT extends TypeTree implements MemberSelectTree {
      private final TypeTree expr;
      private final Name name;

      LocT(TypeTree expr, Name name) {
        this.expr = expr;
        this.name = name;
      }

      @Override
      public Kind getKind() { return Kind.MEMBER_SELECT; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitMemberSelect(this, data);
      }

      @Override
      public TypeTree getExpression() { return expr; }

      @Override
      public Name getIdentifier() { return name; }

      @Override
      public String toString() { return expr + "." + name; }
    }

    static final class ParT extends TypeTree implements ParameterizedTypeTree {
      private final TypeTree base;
      private final List<? extends Tree> typeArgs;

      ParT(TypeTree base, List<? extends Tree> typeArgs) {
        this.base = base;
        this.typeArgs = typeArgs;
      }

      @Override
      public Kind getKind() { return Kind.PARAMETERIZED_TYPE; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitParameterizedType(this, data);
      }

      @Override
      public TypeTree getType() { return base; }

      @Override
      public List<? extends Tree> getTypeArguments() {
        return typeArgs;
      }

      @Override
      public String toString() {
        StringBuilder sb = new StringBuilder(base.toString());
        String s = "<";
        for (Tree t : typeArgs) {
          sb.append(s);
          sb.append(t.toString());
          s = ", ";
        }
        sb.append('>');
        return sb.toString();
      }
    }

    static final class PrimT extends TypeTree implements PrimitiveTypeTree {
      private final TypeKind typeKind;

      PrimT(TypeKind typeKind) {
        this.typeKind = typeKind;
      }

      @Override
      public Kind getKind() { return Kind.PRIMITIVE_TYPE; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitPrimitiveType(this, data);
      }

      @Override
      public TypeKind getPrimitiveTypeKind() { return typeKind; }

      @Override
      public String toString() {
        switch (typeKind) {
        case BOOLEAN: return "boolean";
        case BYTE: return "byte";
        case CHAR: return "char";
        case DOUBLE: return "double";
        case FLOAT: return "float";
        case INT: return "int";
        case LONG: return "long";
        case SHORT: return "short";
        // case VOID: return "void";
        // case WILDCARD: return "?";
        default:
          throw new IllegalArgumentException("unexpected type kind "
              + typeKind);
        }
      }
    }

    static final class IdenT extends TypeTree implements IdentifierTree {
      private final String name;

      IdenT(String dname) {
        this.name = dname;
      }

      @Override
      public Kind getKind() { return Kind.IDENTIFIER; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitIdentifier(this, data);
      }

      @Override
      public Name getName() { return new TypeName(name); }

      @Override
      public String toString() { return name; }
    }

    static final class WildT extends TypeTree implements WildcardTree {
      private final TypeTree bound;
      private final Kind kind;

      WildT() {
        this(Kind.UNBOUNDED_WILDCARD, null);
      }

      WildT(TypeTree bound, BoundedType.BoundKind bk) {
        this(bk == BoundedType.BoundKind.SUPER
                ? Kind.SUPER_WILDCARD
                : Kind.EXTENDS_WILDCARD,
            bound);
      }

      WildT(Kind kind, TypeTree bound) {
        this.kind = kind;
        this.bound = bound;
      }

      @Override
      public Kind getKind() { return kind; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitWildcard(this, data);
      }

      @Override
      public Tree getBound() { return bound; }

      @Override
      public String toString() { return "?"; }
    }

    static final class Param extends TypeTree implements TypeParameterTree {
      private final String bname;
      private final BoundedType.BoundKind bk;
      private final Tree bound;

      Param(String bname, BoundedType.BoundKind bk, TypeTree bound) {
        this.bname = bname;
        this.bk = bk;
        this.bound = bound;
      }

      @Override
      public Kind getKind() { return Kind.TYPE_PARAMETER; }

      @Override
      public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
        return visitor.visitTypeParameter(this, data);
      }

      @Override
      public Name getName() { return new TypeName(bname); }

      @Override
      public List<? extends Tree> getBounds() {
        return Collections.singletonList(bound);
      }

      @Override
      public List<? extends AnnotationTree> getAnnotations() {
        return Collections.emptyList();
      }

      @Override
      public String toString() {
        return bname + " " + bk.toString() + " " + bound.toString();
      }
    }

    static final class TypeName implements Name {
      private final String str;

      TypeName(String str) {
        this.str = str;
      }

      @Override
      public int length() { return str.length(); }

      @Override
      public char charAt(int index) { return str.charAt(index); }

      @Override
      public CharSequence subSequence(int start, int end) {
        return str.subSequence(start, end);
      }

      @Override
      public boolean contentEquals(CharSequence cs) {
        if (cs != null) {
          int n = length();
          if (cs.length() == n) {
            for (int i = 0; i < n; i++) {
              if (charAt(i) != cs.charAt(i)) { return false; }
            }
            return true;
          }
        }
        return false;
      }

      @Override
      public String toString() { return str; }
    }
  }
}