Java程序  |  281行  |  9.74 KB

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

}