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