Home | History | Annotate | Download | only in find
      1 package annotator.find;
      2 
      3 import java.util.*;
      4 import java.util.regex.*;
      5 
      6 import javax.lang.model.element.Name;
      7 
      8 import annotator.scanner.AnonymousClassScanner;
      9 import annotator.scanner.LocalClassScanner;
     10 
     11 import com.sun.source.tree.ClassTree;
     12 import com.sun.source.tree.CompilationUnitTree;
     13 import com.sun.source.tree.ExpressionTree;
     14 import com.sun.source.tree.NewClassTree;
     15 import com.sun.source.tree.Tree;
     16 import com.sun.source.util.TreePath;
     17 
     18 // If there are dollar signs in a name, then there are two
     19 // possibilities regarding how the dollar sign got there.
     20 //  1. Inserted by the compiler, for inner classes.
     21 //  2. Written by the programmer (or by a tool that creates .class files).
     22 // We need to account for both possibilities (and all combinations of them).
     23 
     24 // Example names
     25 //   annotator.tests.FullClassName
     26 //   annotator.tests.FullClassName$InnerClass
     27 //   annotator.tests.FullClassName$0
     28 
     29 /**
     30  * Represents the criterion that a program element is in a class with a
     31  * particular name.
     32  */
     33 public final class InClassCriterion implements Criterion {
     34 
     35   static boolean debug = false;
     36 
     37   public final String className;
     38   private final boolean exactMatch;
     39 
     40   /** The argument is a fully-qualified class name. */
     41   public InClassCriterion(String className, boolean exactMatch) {
     42     this.className = className;
     43     this.exactMatch = exactMatch;
     44   }
     45 
     46   /**
     47    * {@inheritDoc}
     48    */
     49   @Override
     50   public Kind getKind() {
     51     return Kind.IN_CLASS;
     52   }
     53 
     54   /** {@inheritDoc} */
     55   @Override
     56   public boolean isSatisfiedBy(TreePath path, Tree leaf) {
     57     assert path == null || path.getLeaf() == leaf;
     58     return isSatisfiedBy(path);
     59   }
     60 
     61   /** {@inheritDoc} */
     62   @Override
     63   public boolean isSatisfiedBy(TreePath path) {
     64     return InClassCriterion.isSatisfiedBy(path, className, exactMatch);
     65   }
     66 
     67   static Pattern anonclassPattern;
     68   static Pattern localClassPattern;
     69   static {
     70     // for JDK 7: anonclassPattern = Pattern.compile("^(?<num>[0-9]+)(\\$(?<remaining>.*))?$");
     71     anonclassPattern = Pattern.compile("^([0-9]+)(\\$(.*))?$");
     72     localClassPattern = Pattern.compile("^([0-9]+)([^$]+)(\\$(.*))?$");
     73   }
     74 
     75   public static boolean isSatisfiedBy(TreePath path, String className, boolean exactMatch) {
     76     if (path == null) {
     77       return false;
     78     }
     79 
     80     // However much of the class name remains to match.
     81     String cname = className;
     82 
     83     // It is wrong to work from the leaf up to the root of the tree, which
     84     // would fail if the criterion is a.b.c and the actual is a.b.c.c.
     85     List<Tree> trees = new ArrayList<Tree>();
     86     for (Tree tree : path) {
     87       trees.add(tree);
     88     }
     89     Collections.reverse(trees);
     90 
     91     boolean insideMatch = false;
     92     for (int i = 0; i < trees.size(); i++) {
     93       Tree tree = trees.get(i);
     94       boolean checkAnon = false;
     95       boolean checkLocal = false;
     96 
     97       switch (tree.getKind()) {
     98       case COMPILATION_UNIT:
     99         debug("InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    100         ExpressionTree packageTree = ((CompilationUnitTree) tree).getPackageName();
    101         if (packageTree == null) {
    102           // compilation unit is in default package; nothing to do
    103         } else {
    104           String declaredPackage = packageTree.toString();
    105           if (cname.startsWith(declaredPackage + ".")) {
    106             cname = cname.substring(declaredPackage.length()+1);
    107           } else {
    108             debug("false[COMPILATION_UNIT; bad declaredPackage = %s] InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", declaredPackage, cname, tree);
    109             return false;
    110           }
    111         }
    112         break;
    113       case CLASS:
    114       case INTERFACE:
    115       case ENUM:
    116       case ANNOTATION_TYPE:
    117         if (i > 0 && trees.get(i - 1).getKind() == Tree.Kind.NEW_CLASS) {
    118           // For an anonymous class, the CLASS tree is always directly inside of
    119           // a NEW_CLASS tree. If that's the case here then skip this iteration
    120           // since we've already looked at the new class tree in the previous
    121           // iteration.
    122           break;
    123         }
    124         debug("InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    125 
    126         if (i > 0 && trees.get(i - 1).getKind() == Tree.Kind.BLOCK) {
    127           // Section 14.3 of the JLS says "every local class declaration
    128           // statement is immediately contained by a block".
    129           checkLocal = true;
    130           debug("found local class: InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    131           break;
    132         }
    133 
    134         // all four Kinds are represented by ClassTree
    135         ClassTree c = (ClassTree)tree;
    136         Name csn = c.getSimpleName();
    137 
    138         if (csn == null || csn.length() == 0) {
    139           debug("empty getSimpleName: InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    140           checkAnon = true;
    141           break;
    142         }
    143         String treeClassName = csn.toString();
    144         if (cname.equals(treeClassName)) {
    145           if (exactMatch) {
    146             cname = "";
    147           } else {
    148             debug("true InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    149             return true;
    150           }
    151         } else if (cname.startsWith(treeClassName + "$")
    152                    || (cname.startsWith(treeClassName + "."))) {
    153           cname = cname.substring(treeClassName.length()+1);
    154         } else if (!treeClassName.isEmpty()) {
    155           // treeClassName is empty for anonymous inner class
    156           // System.out.println("cname else: " + cname);
    157           debug("false InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    158           return false;
    159         }
    160         break;
    161       case NEW_CLASS:
    162         // When matching the "new Class() { ... }" expression itself, we
    163         // should not use the anonymous class name.  But when matching
    164         // within the braces, we should.
    165         debug("InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    166         if (cname.equals("")) {
    167           insideMatch = true;
    168         } else {
    169           NewClassTree nc = (NewClassTree) tree;
    170           checkAnon = nc.getClassBody() != null;
    171         }
    172         break;
    173       case METHOD:
    174       case VARIABLE:
    175         // Avoid searching inside inner classes of the matching class,
    176         // lest a homographic inner class lead to a spurious match.
    177         if (insideMatch) {
    178           debug("false InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    179           return false;
    180         }
    181         break;
    182       default:
    183         // nothing to do
    184         break;
    185       }
    186 
    187       if (checkAnon) {
    188         // If block is anonymous class, and cname starts with an
    189         // anonymous class index, see if they match.
    190 
    191         Matcher anonclassMatcher = anonclassPattern.matcher(cname);
    192         if (! anonclassMatcher.matches()) {
    193           debug("false[anonclassMatcher] InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    194           return false;
    195         }
    196         // for JDK 7: String anonclassNumString = anonclassMatcher.group("num");
    197         // for JDK 7: cname = anonclassMatcher.group("remaining");
    198         String anonclassNumString = anonclassMatcher.group(1);
    199         cname = anonclassMatcher.group(3);
    200         if (cname == null) {
    201           cname = "";
    202         }
    203         int anonclassNum;
    204         try {
    205           anonclassNum = Integer.parseInt(anonclassNumString);
    206         } catch (NumberFormatException e) {
    207           throw new Error("This can't happen: " + cname + "$" + anonclassNumString, e);
    208         }
    209 
    210         int actualIndexInSource = AnonymousClassScanner.indexOfClassTree(path, tree);
    211 
    212         if (anonclassNum != actualIndexInSource) {
    213           debug("false[anonclassNum %d %d] InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", anonclassNum, actualIndexInSource, cname, tree);
    214           return false;
    215         }
    216       } else if (checkLocal) {
    217         ClassTree c = (ClassTree) tree;
    218         String treeClassName = c.getSimpleName().toString();
    219 
    220         Matcher localClassMatcher = localClassPattern.matcher(cname);
    221         if (!localClassMatcher.matches()) {
    222           debug("false[localClassMatcher] InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname, tree);
    223           return false;
    224         }
    225         String localClassNumString = localClassMatcher.group(1);
    226         String localClassName = localClassMatcher.group(2);
    227         int localClassNum = Integer.parseInt(localClassNumString);
    228 
    229         int actualIndexInSource = LocalClassScanner.indexOfClassTree(path, c);
    230 
    231         if (actualIndexInSource == localClassNum && treeClassName.startsWith(localClassName)) {
    232           cname = localClassMatcher.group(4);
    233           if (cname == null) {
    234             cname = "";
    235           }
    236         } else {
    237           debug("false[localClassNum %d %d] InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", localClassNum, actualIndexInSource, cname, tree);
    238           return false;
    239         }
    240       }
    241     }
    242 
    243     debug("%s InClassCriterion.isSatisfiedBy:%n  cname=%s%n  tree=%s%n", cname.equals(""), cname, path.getLeaf());
    244     return cname.equals("");
    245   }
    246 
    247   /**
    248    * {@inheritDoc}
    249    */
    250   @Override
    251   public String toString() {
    252     return "In class '" + className + "'" + (exactMatch ? " (exactly)" : "");
    253   }
    254 
    255   /**
    256    * Return an array of Strings representing the characters between
    257    * successive instances of the delimiter character.
    258    * Always returns an array of length at least 1 (it might contain only the
    259    * empty string).
    260    * @see #split(String s, String delim)
    261    */
    262   /*
    263   private static List<String> split(String s, char delim) {
    264     List<String> result = new ArrayList<String>();
    265     for (int delimpos = s.indexOf(delim); delimpos != -1; delimpos = s.indexOf(delim)) {
    266       result.add(s.substring(0, delimpos));
    267       s = s.substring(delimpos+1);
    268     }
    269     result.add(s);
    270     return result;
    271   }
    272   */
    273 
    274   private static void debug(String message, Object... args) {
    275     if (debug) {
    276       System.out.printf(message, args);
    277     }
    278   }
    279 
    280 }
    281