package annotator.find; import java.util.*; import java.util.regex.*; import javax.lang.model.element.Name; import annotator.scanner.AnonymousClassScanner; import annotator.scanner.LocalClassScanner; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; // If there are dollar signs in a name, then there are two // possibilities regarding how the dollar sign got there. // 1. Inserted by the compiler, for inner classes. // 2. Written by the programmer (or by a tool that creates .class files). // We need to account for both possibilities (and all combinations of them). // Example names // annotator.tests.FullClassName // annotator.tests.FullClassName$InnerClass // annotator.tests.FullClassName$0 /** * Represents the criterion that a program element is in a class with a * particular name. */ public final class InClassCriterion implements Criterion { static boolean debug = false; public final String className; private final boolean exactMatch; /** The argument is a fully-qualified class name. */ public InClassCriterion(String className, boolean exactMatch) { this.className = className; this.exactMatch = exactMatch; } /** * {@inheritDoc} */ @Override public Kind getKind() { return Kind.IN_CLASS; } /** {@inheritDoc} */ @Override public boolean isSatisfiedBy(TreePath path, Tree leaf) { assert path == null || path.getLeaf() == leaf; return isSatisfiedBy(path); } /** {@inheritDoc} */ @Override public boolean isSatisfiedBy(TreePath path) { return InClassCriterion.isSatisfiedBy(path, className, exactMatch); } static Pattern anonclassPattern; static Pattern localClassPattern; static { // for JDK 7: anonclassPattern = Pattern.compile("^(?<num>[0-9]+)(\\$(?<remaining>.*))?$"); anonclassPattern = Pattern.compile("^([0-9]+)(\\$(.*))?$"); localClassPattern = Pattern.compile("^([0-9]+)([^$]+)(\\$(.*))?$"); } public static boolean isSatisfiedBy(TreePath path, String className, boolean exactMatch) { if (path == null) { return false; } // However much of the class name remains to match. String cname = className; // It is wrong to work from the leaf up to the root of the tree, which // would fail if the criterion is a.b.c and the actual is a.b.c.c. List<Tree> trees = new ArrayList<Tree>(); for (Tree tree : path) { trees.add(tree); } Collections.reverse(trees); boolean insideMatch = false; for (int i = 0; i < trees.size(); i++) { Tree tree = trees.get(i); boolean checkAnon = false; boolean checkLocal = false; switch (tree.getKind()) { case COMPILATION_UNIT: debug("InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); ExpressionTree packageTree = ((CompilationUnitTree) tree).getPackageName(); if (packageTree == null) { // compilation unit is in default package; nothing to do } else { String declaredPackage = packageTree.toString(); if (cname.startsWith(declaredPackage + ".")) { cname = cname.substring(declaredPackage.length()+1); } else { debug("false[COMPILATION_UNIT; bad declaredPackage = %s] InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", declaredPackage, cname, tree); return false; } } break; case CLASS: case INTERFACE: case ENUM: case ANNOTATION_TYPE: if (i > 0 && trees.get(i - 1).getKind() == Tree.Kind.NEW_CLASS) { // For an anonymous class, the CLASS tree is always directly inside of // a NEW_CLASS tree. If that's the case here then skip this iteration // since we've already looked at the new class tree in the previous // iteration. break; } debug("InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); if (i > 0 && trees.get(i - 1).getKind() == Tree.Kind.BLOCK) { // Section 14.3 of the JLS says "every local class declaration // statement is immediately contained by a block". checkLocal = true; debug("found local class: InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); break; } // all four Kinds are represented by ClassTree ClassTree c = (ClassTree)tree; Name csn = c.getSimpleName(); if (csn == null || csn.length() == 0) { debug("empty getSimpleName: InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); checkAnon = true; break; } String treeClassName = csn.toString(); if (cname.equals(treeClassName)) { if (exactMatch) { cname = ""; } else { debug("true InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); return true; } } else if (cname.startsWith(treeClassName + "$") || (cname.startsWith(treeClassName + "."))) { cname = cname.substring(treeClassName.length()+1); } else if (!treeClassName.isEmpty()) { // treeClassName is empty for anonymous inner class // System.out.println("cname else: " + cname); debug("false InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); return false; } break; case NEW_CLASS: // When matching the "new Class() { ... }" expression itself, we // should not use the anonymous class name. But when matching // within the braces, we should. debug("InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); if (cname.equals("")) { insideMatch = true; } else { NewClassTree nc = (NewClassTree) tree; checkAnon = nc.getClassBody() != null; } break; case METHOD: case VARIABLE: // Avoid searching inside inner classes of the matching class, // lest a homographic inner class lead to a spurious match. if (insideMatch) { debug("false InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); return false; } break; default: // nothing to do break; } if (checkAnon) { // If block is anonymous class, and cname starts with an // anonymous class index, see if they match. Matcher anonclassMatcher = anonclassPattern.matcher(cname); if (! anonclassMatcher.matches()) { debug("false[anonclassMatcher] InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); return false; } // for JDK 7: String anonclassNumString = anonclassMatcher.group("num"); // for JDK 7: cname = anonclassMatcher.group("remaining"); String anonclassNumString = anonclassMatcher.group(1); cname = anonclassMatcher.group(3); if (cname == null) { cname = ""; } int anonclassNum; try { anonclassNum = Integer.parseInt(anonclassNumString); } catch (NumberFormatException e) { throw new Error("This can't happen: " + cname + "$" + anonclassNumString, e); } int actualIndexInSource = AnonymousClassScanner.indexOfClassTree(path, tree); if (anonclassNum != actualIndexInSource) { debug("false[anonclassNum %d %d] InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", anonclassNum, actualIndexInSource, cname, tree); return false; } } else if (checkLocal) { ClassTree c = (ClassTree) tree; String treeClassName = c.getSimpleName().toString(); Matcher localClassMatcher = localClassPattern.matcher(cname); if (!localClassMatcher.matches()) { debug("false[localClassMatcher] InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname, tree); return false; } String localClassNumString = localClassMatcher.group(1); String localClassName = localClassMatcher.group(2); int localClassNum = Integer.parseInt(localClassNumString); int actualIndexInSource = LocalClassScanner.indexOfClassTree(path, c); if (actualIndexInSource == localClassNum && treeClassName.startsWith(localClassName)) { cname = localClassMatcher.group(4); if (cname == null) { cname = ""; } } else { debug("false[localClassNum %d %d] InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", localClassNum, actualIndexInSource, cname, tree); return false; } } } debug("%s InClassCriterion.isSatisfiedBy:%n cname=%s%n tree=%s%n", cname.equals(""), cname, path.getLeaf()); return cname.equals(""); } /** * {@inheritDoc} */ @Override public String toString() { return "In class '" + className + "'" + (exactMatch ? " (exactly)" : ""); } /** * Return an array of Strings representing the characters between * successive instances of the delimiter character. * Always returns an array of length at least 1 (it might contain only the * empty string). * @see #split(String s, String delim) */ /* private static List<String> split(String s, char delim) { List<String> result = new ArrayList<String>(); for (int delimpos = s.indexOf(delim); delimpos != -1; delimpos = s.indexOf(delim)) { result.add(s.substring(0, delimpos)); s = s.substring(delimpos+1); } result.add(s); return result; } */ private static void debug(String message, Object... args) { if (debug) { System.out.printf(message, args); } } }