| package annotator.find; |
| |
| import annotator.Main; |
| |
| import java.util.ArrayList; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import plume.UtilMDE; |
| |
| import com.sun.source.tree.AnnotatedTypeTree; |
| import com.sun.source.tree.ClassTree; |
| import com.sun.source.tree.CompilationUnitTree; |
| import com.sun.source.tree.ExpressionTree; |
| import com.sun.source.tree.ImportTree; |
| import com.sun.source.tree.MethodTree; |
| import com.sun.source.tree.Tree; |
| import com.sun.source.tree.TypeParameterTree; |
| import com.sun.source.tree.VariableTree; |
| import com.sun.source.util.TreePath; |
| |
| public class IsSigMethodCriterion implements Criterion { |
| |
| // The context is used for determining the fully qualified name of methods. |
| private static class Context { |
| public final String packageName; |
| public final List<String> imports; |
| public Context(String packageName, List<String> imports) { |
| this.packageName = packageName; |
| this.imports = imports; |
| } |
| } |
| |
| private static final Map<CompilationUnitTree, Context> contextCache = new HashMap<CompilationUnitTree, Context>(); |
| |
| private final String fullMethodName; // really the full JVML signature, sans return type |
| private final String simpleMethodName; |
| // list of parameters in Java, not JVML format |
| private final List<String> fullyQualifiedParams; |
| // in Java, not JVML, format. may be "void" |
| private final String returnType; |
| |
| public IsSigMethodCriterion(String methodName) { |
| this.fullMethodName = methodName.substring(0, methodName.indexOf(")") + 1); |
| this.simpleMethodName = methodName.substring(0, methodName.indexOf("(")); |
| // this.fullyQualifiedParams = new ArrayList<String>(); |
| // for (String s : methodName.substring( |
| // methodName.indexOf("(") + 1, methodName.indexOf(")")).split(",")) { |
| // if (s.length() > 0) { |
| // fullyQualifiedParams.add(s); |
| // } |
| // } |
| this.fullyQualifiedParams = new ArrayList<String>(); |
| try { |
| parseParams( |
| methodName.substring(methodName.indexOf("(") + 1, |
| methodName.indexOf(")"))); |
| } catch (Exception e) { |
| throw new RuntimeException("Caught exception while parsing method: " + |
| methodName, e); |
| } |
| String returnTypeJvml = methodName.substring(methodName.indexOf(")") + 1); |
| this.returnType = (returnTypeJvml.equals("V") |
| ? "void" |
| : UtilMDE.fieldDescriptorToBinaryName(returnTypeJvml)); |
| } |
| |
| // params is in JVML format |
| private void parseParams(String params) { |
| while (params.length() != 0) { |
| // nextParam is in JVML format |
| String nextParam = readNext(params); |
| params = params.substring(nextParam.length()); |
| fullyQualifiedParams.add(UtilMDE.fieldDescriptorToBinaryName(nextParam)); |
| } |
| } |
| |
| // strip a JVML type off a string containing multiple concatenated JVML types |
| private String readNext(String restOfParams) { |
| String firstChar = restOfParams.substring(0, 1); |
| if (isPrimitiveLetter(firstChar)) { |
| return firstChar; |
| } else if (firstChar.equals("[")) { |
| return "[" + readNext(restOfParams.substring(1)); |
| } else if (firstChar.equals("L")) { |
| return "L" + restOfParams.substring(1, restOfParams.indexOf(";") + 1); |
| } else { |
| throw new RuntimeException("Unknown method params: " + fullMethodName + " with remainder: " + restOfParams); |
| } |
| } |
| |
| // called by isSatisfiedBy(TreePath), will get compilation unit on its own |
| private static Context initImports(TreePath path) { |
| CompilationUnitTree topLevel = path.getCompilationUnit(); |
| Context result = contextCache.get(topLevel); |
| if (result != null) { |
| return result; |
| } |
| |
| ExpressionTree packageTree = topLevel.getPackageName(); |
| String packageName; |
| if (packageTree == null) { |
| packageName = ""; // the default package |
| } else { |
| packageName = packageTree.toString(); |
| } |
| |
| List<String> imports = new ArrayList<String>(); |
| for (ImportTree i : topLevel.getImports()) { |
| String imported = i.getQualifiedIdentifier().toString(); |
| imports.add(imported); |
| } |
| |
| result = new Context(packageName, imports); |
| contextCache.put(topLevel, result); |
| return result; |
| } |
| |
| // Abstracts out the inner loop of matchTypeParams. |
| // goalType is fully-qualified. |
| private boolean matchTypeParam(String goalType, Tree type, |
| Map<String, String> typeToClassMap, |
| Context context) { |
| String simpleType = type.toString(); |
| |
| boolean haveMatch = matchSimpleType(goalType, simpleType, context); |
| if (!haveMatch) { |
| if (!typeToClassMap.isEmpty()) { |
| for (Map.Entry<String, String> p : typeToClassMap.entrySet()) { |
| simpleType = simpleType.replaceAll("\\b" + p.getKey() + "\\b", |
| p.getValue()); |
| haveMatch = matchSimpleType(goalType, simpleType, context); |
| if (!haveMatch) { |
| Criteria.dbug.debug("matchTypeParams() => false:%n"); |
| Criteria.dbug.debug(" type = %s%n", type); |
| Criteria.dbug.debug(" simpleType = %s%n", simpleType); |
| Criteria.dbug.debug(" goalType = %s%n", goalType); |
| } |
| } |
| } |
| } |
| return haveMatch; |
| } |
| |
| |
| private boolean matchTypeParams(List<? extends VariableTree> sourceParams, |
| Map<String, String> typeToClassMap, |
| Context context) { |
| assert sourceParams.size() == fullyQualifiedParams.size(); |
| for (int i = 0; i < sourceParams.size(); i++) { |
| String fullType = fullyQualifiedParams.get(i); |
| VariableTree vt = sourceParams.get(i); |
| Tree vtType = vt.getType(); |
| if (! matchTypeParam(fullType, vtType, typeToClassMap, context)) { |
| Criteria.dbug.debug( |
| "matchTypeParam() => false:%n i=%d vt = %s%n fullType = %s%n", |
| i, vt, fullType); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| |
| // simpleType is the name as it appeared in the source code. |
| // fullType is fully-qualified. |
| // Both are in Java, not JVML, format. |
| private boolean matchSimpleType(String fullType, String simpleType, Context context) { |
| Criteria.dbug.debug("matchSimpleType(%s, %s, %s)%n", |
| fullType, simpleType, context); |
| |
| // must strip off generics, is all of this necessary, though? |
| // do you ever have generics anywhere but at the end? |
| while (simpleType.contains("<")) { |
| int bracketIndex = simpleType.lastIndexOf("<"); |
| String beforeBracket = simpleType.substring(0, bracketIndex); |
| String afterBracket = simpleType.substring(simpleType.indexOf(">", bracketIndex) + 1); |
| simpleType = beforeBracket + afterBracket; |
| } |
| |
| |
| // TODO: arrays? |
| |
| // first try qualifying simpleType with this package name, |
| // then with java.lang |
| // then with default package |
| // then with all of the imports |
| |
| boolean matchable = false; |
| |
| if (!matchable) { |
| // match with this package name |
| String packagePrefix = context.packageName; |
| if (packagePrefix.length() > 0) { |
| packagePrefix = packagePrefix + "."; |
| } |
| if (matchWithPrefix(fullType, simpleType, packagePrefix)) { |
| matchable = true; |
| } |
| } |
| |
| if (!matchable) { |
| // match with java.lang |
| if (matchWithPrefix(fullType, simpleType, "java.lang.")) { |
| matchable = true; |
| } |
| } |
| |
| if (!matchable) { |
| // match with default package |
| if (matchWithPrefix(fullType, simpleType, "")) { |
| matchable = true; |
| } |
| } |
| |
| /* |
| * From Java 7 language definition 6.5.5.2 (Qualified Types): |
| * If a type name is of the form Q.Id, then Q must be either a type |
| * name or a package name. If Id names exactly one accessible type |
| * that is a member of the type or package denoted by Q, then the |
| * qualified type name denotes that type. |
| */ |
| if (!matchable) { |
| // match with any of the imports |
| for (String someImport : context.imports) { |
| String importPrefix = null; |
| if (someImport.contains("*")) { |
| // don't include the * in the prefix, should end in . |
| // TODO: this is a real bug due to nonnull, though I discovered it manually |
| // importPrefix = someImport.substring(0, importPrefix.indexOf("*")); |
| importPrefix = someImport.substring(0, someImport.indexOf("*")); |
| } else { |
| // if you imported a specific class, you can only use that import |
| // if the last part matches the simple type |
| String importSimpleType = |
| someImport.substring(someImport.lastIndexOf(".") + 1); |
| |
| // Remove array brackets from simpleType if it has them |
| int arrayBracket = simpleType.indexOf('['); |
| String simpleBaseType = simpleType; |
| if (arrayBracket > -1) { |
| simpleBaseType = simpleType.substring(0, arrayBracket); |
| } |
| if (!(simpleBaseType.equals(importSimpleType) |
| || simpleBaseType.startsWith(importSimpleType + "."))) { |
| continue; |
| } |
| |
| importPrefix = someImport.substring(0, someImport.lastIndexOf(".") + 1); |
| } |
| |
| if (matchWithPrefix(fullType, simpleType, importPrefix)) { |
| matchable = true; |
| break; // out of for loop |
| } |
| } |
| } |
| |
| return matchable; |
| } |
| |
| private boolean matchWithPrefix(String fullType, String simpleType, String prefix) { |
| return matchWithPrefixOneWay(fullType, simpleType, prefix) |
| || matchWithPrefixOneWay(simpleType, fullType, prefix); |
| } |
| |
| // simpleType can be in JVML format ?? Is that really possible? |
| private boolean matchWithPrefixOneWay(String fullType, String simpleType, |
| String prefix) { |
| |
| // maybe simpleType is in JVML format |
| String simpleType2 = simpleType.replace("/", "."); |
| |
| String fullType2 = fullType.replace("$", "."); |
| |
| /* unused String prefix2 = (prefix.endsWith(".") |
| ? prefix.substring(0, prefix.length() - 1) |
| : prefix); */ |
| boolean b = (fullType2.equals(prefix + simpleType2) |
| // Hacky way to handle the possibility that fulltype is an |
| // inner type but simple type is unqualified. |
| || (fullType.startsWith(prefix) |
| && (fullType.endsWith("$" + simpleType2) |
| || fullType2.endsWith("." + simpleType2)))); |
| Criteria.dbug.debug("matchWithPrefix(%s, %s, %s) => %b)%n", |
| fullType2, simpleType, prefix, b); |
| return b; |
| } |
| |
| /** {@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) { |
| if (path == null) { |
| return false; |
| } |
| |
| Context context = initImports(path); |
| |
| Tree leaf = path.getLeaf(); |
| |
| if (leaf.getKind() != Tree.Kind.METHOD) { |
| Criteria.dbug.debug( |
| "IsSigMethodCriterion.isSatisfiedBy(%s) => false: not a METHOD tree%n", |
| Main.pathToString(path)); |
| return false; |
| } |
| // else if ((((JCMethodDecl) leaf).mods.flags & Flags.GENERATEDCONSTR) != 0) { |
| // Criteria.dbug.debug( |
| // "IsSigMethodCriterion.isSatisfiedBy(%s) => false: generated constructor%n", |
| // Main.pathToString(path)); |
| // return false; |
| // } |
| |
| MethodTree mt = (MethodTree) leaf; |
| |
| if (! simpleMethodName.equals(mt.getName().toString())) { |
| Criteria.dbug.debug("IsSigMethodCriterion.isSatisfiedBy => false: Names don't match%n"); |
| return false; |
| } |
| |
| List<? extends VariableTree> sourceParams = mt.getParameters(); |
| if (fullyQualifiedParams.size() != sourceParams.size()) { |
| Criteria.dbug.debug("IsSigMethodCriterion.isSatisfiedBy => false: Number of parameters don't match%n"); |
| return false; |
| } |
| |
| // now go through all type parameters declared by method |
| // and for each one, create a mapping from the type to the |
| // first declared extended class, defaulting to Object |
| // for example, |
| // <T extends Date> void foo(T t) |
| // creates mapping: T -> Date |
| // <T extends Date & List> void foo(Object o) |
| // creates mapping: T -> Date |
| // <T extends Date, U extends List> foo(Object o) |
| // creates mappings: T -> Date, U -> List |
| // <T> void foo(T t) |
| // creates mapping: T -> Object |
| |
| Map<String, String> typeToClassMap = new HashMap<String, String>(); |
| for (TypeParameterTree param : mt.getTypeParameters()) { |
| String paramName = param.getName().toString(); |
| String paramClass = "Object"; |
| List<? extends Tree> paramBounds = param.getBounds(); |
| if (paramBounds != null && paramBounds.size() >= 1) { |
| Tree boundZero = paramBounds.get(0); |
| if (boundZero.getKind() == Tree.Kind.ANNOTATED_TYPE) { |
| boundZero = ((AnnotatedTypeTree) boundZero).getUnderlyingType(); |
| } |
| paramClass = boundZero.toString(); |
| } |
| typeToClassMap.put(paramName, paramClass); |
| } |
| |
| // Do the same for the enclosing class. |
| // The type variable might not be from the directly enclosing |
| // class, but from a further up class. |
| // Go through all enclosing classes and add the type parameters. |
| { |
| TreePath classpath = path; |
| ClassTree ct = enclosingClass(classpath); |
| while (ct!=null) { |
| for (TypeParameterTree param : ct.getTypeParameters()) { |
| String paramName = param.getName().toString(); |
| String paramClass = "Object"; |
| List<? extends Tree> paramBounds = param.getBounds(); |
| if (paramBounds != null && paramBounds.size() >= 1) { |
| Tree pb = paramBounds.get(0); |
| if (pb.getKind() == Tree.Kind.ANNOTATED_TYPE) { |
| pb = ((AnnotatedTypeTree)pb).getUnderlyingType(); |
| } |
| paramClass = pb.toString(); |
| } |
| typeToClassMap.put(paramName, paramClass); |
| } |
| classpath = classpath.getParentPath(); |
| ct = enclosingClass(classpath); |
| } |
| } |
| |
| if (! matchTypeParams(sourceParams, typeToClassMap, context)) { |
| Criteria.dbug.debug("IsSigMethodCriterion => false: Parameter types don't match%n"); |
| return false; |
| } |
| |
| if ((mt.getReturnType() != null) // must be a constructor |
| && (! matchTypeParam(returnType, mt.getReturnType(), typeToClassMap, context))) { |
| Criteria.dbug.debug("IsSigMethodCriterion => false: Return types don't match%n"); |
| return false; |
| } |
| |
| Criteria.dbug.debug("IsSigMethodCriterion.isSatisfiedBy => true%n"); |
| return true; |
| } |
| |
| /* This is a copy of the method from the Checker Framework |
| * TreeUtils.enclosingClass. |
| * We cannot have a dependency on the Checker Framework. |
| * TODO: as is the case there, anonymous classes are not handled correctly. |
| */ |
| private static ClassTree enclosingClass(final TreePath path) { |
| final Set<Tree.Kind> kinds = EnumSet.of( |
| Tree.Kind.CLASS, |
| Tree.Kind.ENUM, |
| Tree.Kind.INTERFACE, |
| Tree.Kind.ANNOTATION_TYPE |
| ); |
| TreePath p = path; |
| |
| while (p != null) { |
| Tree leaf = p.getLeaf(); |
| assert leaf != null; /*nninvariant*/ |
| if (kinds.contains(leaf.getKind())) { |
| return (ClassTree) leaf; |
| } |
| p = p.getParentPath(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public Kind getKind() { |
| return Kind.SIG_METHOD; |
| } |
| |
| // public static String getSignature(MethodTree mt) { |
| // String sig = mt.getName().toString().trim(); // method name, no parameters |
| // sig += "("; |
| // boolean first = true; |
| // for (VariableTree vt : mt.getParameters()) { |
| // if (!first) { |
| // sig += ","; |
| // } |
| // sig += getType(vt.getType()); |
| // first = false; |
| // } |
| // sig += ")"; |
| // |
| // return sig; |
| // } |
| // |
| // private static String getType(Tree t) { |
| // if (t.getKind() == Tree.Kind.PRIMITIVE_TYPE) { |
| // return getPrimitiveType((PrimitiveTypeTree) t); |
| // } else if (t.getKind() == Tree.Kind.IDENTIFIER) { |
| // return "L" + ((IdentifierTree) t).getName().toString(); |
| // } else if (t.getKind() == Tree.Kind.PARAMETERIZED_TYPE) { |
| // // don't care about generics due to erasure |
| // return getType(((ParameterizedTypeTree) t).getType()); |
| // } |
| // throw new RuntimeException("unable to get type of: " + t); |
| // } |
| // |
| // private static String getPrimitiveType(PrimitiveTypeTree pt) { |
| // TypeKind tk = pt.getPrimitiveTypeKind(); |
| // if (tk == TypeKind.ARRAY) { |
| // return "["; |
| // } else if (tk == TypeKind.BOOLEAN) { |
| // return "Z"; |
| // } else if (tk == TypeKind.BYTE) { |
| // return "B"; |
| // } else if (tk == TypeKind.CHAR) { |
| // return "C"; |
| // } else if (tk == TypeKind.DOUBLE) { |
| // return "D"; |
| // } else if (tk == TypeKind.FLOAT) { |
| // return "F"; |
| // } else if (tk == TypeKind.INT) { |
| // return "I"; |
| // } else if (tk == TypeKind.LONG) { |
| // return "J"; |
| // } else if (tk == TypeKind.SHORT) { |
| // return "S"; |
| // } |
| // |
| // throw new RuntimeException("Invalid TypeKind: " + tk); |
| // } |
| |
| /* |
| private boolean isPrimitive(String s) { |
| return |
| s.equals("boolean") || |
| s.equals("byte") || |
| s.equals("char") || |
| s.equals("double") || |
| s.equals("float") || |
| s.equals("int") || |
| s.equals("long") || |
| s.equals("short"); |
| } |
| */ |
| |
| private boolean isPrimitiveLetter(String s) { |
| return |
| s.equals("Z") || |
| s.equals("B") || |
| s.equals("C") || |
| s.equals("D") || |
| s.equals("F") || |
| s.equals("I") || |
| s.equals("J") || |
| s.equals("S"); |
| } |
| |
| /* |
| private String primitiveLetter(String s) { |
| if (s.equals("boolean")) { |
| return "Z"; |
| } else if (s.equals("byte")) { |
| return "B"; |
| } else if (s.equals("char")) { |
| return "C"; |
| } else if (s.equals("double")) { |
| return "D"; |
| } else if (s.equals("float")) { |
| return "F"; |
| } else if (s.equals("int")) { |
| return "I"; |
| } else if (s.equals("long")) { |
| return "J"; |
| } else if (s.equals("short")) { |
| return "S"; |
| } else { |
| throw new RuntimeException("IsSigMethodCriterion: unknown primitive: " + s); |
| } |
| } |
| */ |
| |
| @Override |
| public String toString() { |
| return "IsSigMethodCriterion: " + fullMethodName; |
| } |
| } |