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