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);
}
}
}