package com.laytonsmith.PureUtilities.Common.Annotations; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.ClassLoading.ClassMirror.ClassMirror; import com.laytonsmith.PureUtilities.Common.ClassUtils; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.annotations.MustUseOverride; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; /** * */ @SupportedAnnotationTypes({"java.lang.Override", "com.laytonsmith.annotations.MustUseOverride"}) @SupportedSourceVersion(SourceVersion.RELEASE_7) public class CheckOverrides extends AbstractProcessor { private static final boolean enabled = true; private static Map<Class, Set<Method>> methods = null; private static final Set<Class> interfacesWithMustUseOverride = new HashSet<>(); private static final Pattern METHOD_SIGNATURE = Pattern.compile("[a-zA-Z0-9_]+\\((.*)\\)"); private static final Pattern CLASS_TEMPLATES = Pattern.compile("^.*?<(.*)>?$"); @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if(!enabled){ processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "CheckOverrides processor is turned off!"); return false; } setup(); if (!roundEnv.processingOver()) { for (Element element : roundEnv.getElementsAnnotatedWith(MustUseOverride.class)) { String className = element.toString(); Class c = null; try { c = getClassFromName(className); } catch (ClassNotFoundException ex) { Logger.getLogger(CheckOverrides.class.getName()).log(Level.SEVERE, null, ex); } if (c != null) { if (!c.isInterface()) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only interfaces may be annotated with " + MustUseOverride.class.getName()); } interfacesWithMustUseOverride.add(c); } } for (Element element : roundEnv.getElementsAnnotatedWith(Override.class)) { String className = element.getEnclosingElement().toString(); Class c = null; try { c = getClassFromName(className); } catch (ClassNotFoundException ex) { Logger.getLogger(CheckOverrides.class.getName()).log(Level.SEVERE, null, ex); } if (c != null && !c.isInterface()) { //StreamUtils.GetSystemOut().println("Dealing with " + c.getName() + ".." + element.toString()); //We have to do a bit of massaging to turn "method(java.lang.String[], java.lang.String) //into a Method object. Matcher m = METHOD_SIGNATURE.matcher(element.toString()); String methodName = element.getSimpleName().toString(); Class[] argTypes; boolean isTemplate = false; if (!m.find()) { argTypes = new Class[0]; } else { String inner = m.group(1); String[] args; if ("".equals(inner.trim())) { args = new String[0]; } else { //Take out generics, since we can't really deal with them, and they make parsing //the args harder. inner = removeGenerics(inner); args = StringUtils.trimSplit(inner, ","); } //StreamUtils.GetSystemOut().println("Args length: " + args.length); argTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { try { argTypes[i] = getClassFromName(args[i]); } catch (ClassNotFoundException e) { //It may be a template parameter, so check in the enclosing class name for that //template type. String codeClassName = element.getEnclosingElement().asType().toString(); Matcher mm = CLASS_TEMPLATES.matcher(codeClassName); boolean found = false; if (mm.find()) { String[] templates = removeGenerics(mm.group(1)).split(","); String baseClass = args[i].replaceAll("\\[\\]", ""); for (String template : templates) { if (baseClass.equals(template)) { //Ok, it's found. isTemplate = true; found = true; args[i] = args[i].replaceFirst(Pattern.quote(template), "java.lang.Object"); break; } } } if (!isTemplate || !found) { //Oh, there aren't any. Well, I don't know why this would happen. Logger.getLogger(CheckOverrides.class.getName()).log(Level.SEVERE, null, e); } try{ argTypes[i] = Class.forName(args[i]); } catch(ClassNotFoundException ex){ //Won't happen } } } } if (isTemplate) { //Template parameters that extend something break this, because the annotation //processor doesn't provide the information to us. So, for instance, if you have //a template parameter MyClass<T extends List> and a method in that class //void myMethod(T); then the signature of that method is actually //void myMethod(List), but since we don't have a way of getting "List" //from the APT, we can't really do anything to detect if you've overridden //void myMethod(T) vs void myMethod(Object). So we have to settle here for //missing an error, instead of erroring out when there isn't actually one, //and remove all the methods with this name and type. We can, however, //avoid removing methods with different number of arguments, since we //can guarantee those aren't overridden. for(Method method : c.getDeclaredMethods()){ if(method.getName().equals(methodName) && method.getParameterTypes().length == argTypes.length){ methods.get(c).remove(method); } } } else { //Arg types are now all provided, and so is the method name. try { Method method = c.getDeclaredMethod(methodName, argTypes); //Ok, remove it from the list of methods, cause we know it's overridden. //.remove won't work, because we need to also remove co-return types present Iterator<Method> it = methods.get(c).iterator(); while (it.hasNext()) { Method next = it.next(); if (next.getName().equals(method.getName()) && checkSignatureForCompatibility(next.getParameterTypes(), method.getParameterTypes())) { it.remove(); } } } catch (NoSuchMethodException | SecurityException ex) { Logger.getLogger(CheckOverrides.class.getName()).log(Level.SEVERE, null, ex); } } if (methods.get(c).isEmpty()) { methods.remove(c); } } } //Now all the overridden methods have been removed from the list of methods in all the //classes. We now need to go through and find out which of the remaining methods *could* //be overriden, as many may not be overrides anyways. Set<Method> methodsInError = new HashSet<>(); for (Class c : methods.keySet()) { Set<Method> mm = methods.get(c); for (Method m : mm) { //Get the superclass/superinterfaces that this class extends/implements //all the way up to Object Set<Class> supers = new HashSet<>(); getAllSupers(c, supers, true); //Ok, now look through all the superclasses' methods, and find any that //match the signature. If they do, it's an error. List<Method> compare = new ArrayList<>(); for (Class s : supers) { compare.addAll(getOverridableMethods(s)); } for (Method superM : compare) { if (m.getName().equals(superM.getName())) { if (checkSignatureForCompatibility(superM.getParameterTypes(), m.getParameterTypes())) { //Oops, found a bad method. methodsInError.add(m); } } //else different method altogether } } } if (!methodsInError.isEmpty()) { //Some package names are pretty verbose, and will more than //likely be included with an import, so let's trim the //error message down some so it's easier to read final List<String> verbosePackages = Arrays.asList(new String[]{ "java.lang", "java.util", "java.io" }); //Build a sorted set, so these go in order. SortedSet<String> stringMethodsInError = new TreeSet<>(); for (Method m : methodsInError) { stringMethodsInError.add(m.getDeclaringClass().getName() + "." + m.getName() + "(" + StringUtils.Join(Arrays.asList(m.getParameterTypes()), ", ", ", ", ", ", "", new StringUtils.Renderer<Class<?>>() { @Override public String toString(Class<?> item) { String name = ClassUtils.getCommonName(item); for (String v : verbosePackages) { if (name.matches(Pattern.quote(v) + "\\.([^\\.]*?)$")) { return name.replaceFirst(Pattern.quote(v) + "\\.", ""); } } return name; } }) + ")"); } final StringBuilder b = new StringBuilder(); b.append("There ") .append(StringUtils.PluralTemplateHelper(stringMethodsInError.size(), "is a method which overrides or implements a method in a super class/super interface," + " but doesn't use the @Override tag. Please tag this method", "are %d methods which override or implement a method in a super class/super interface" + " but don't use the @Override tag. Please tag these methods")) .append(" with @Override to continue the build process.") .append(StringUtils.nl) .append(StringUtils.Join(stringMethodsInError, StringUtils.nl)); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, b.toString()); } else { StreamUtils.GetSystemOut().println("No @Override annotations were found to be missing."); } } return false; } private static void getAllSupers(Class c, Set<Class> building, boolean first) { if (c == null || c == Object.class) { return; } if (!first) { building.add(c); } else { //everything extends Object, and we're gonna use //that as our stop marker later, so go ahead and //add this now. building.add(Object.class); } getAllSupers(c.getSuperclass(), building, false); for (Class cc : c.getInterfaces()) { if (interfacesWithMustUseOverride.contains(cc)) { building.add(cc); } } } /** * Checks to see if an argument signature is compatible. That is, if the * parameter types match. * * @param superArgs The super class arguments to check * @param subArgs The sub class arguments to check * @return */ private static boolean checkSignatureForCompatibility(Class[] superArgs, Class[] subArgs) { if (superArgs.length != subArgs.length) { return false; } for (int i = 0; i < superArgs.length; i++) { if (superArgs[i] != subArgs[i]) { return false; } } return true; } /** * Removes generics from an identifier. * * @param identifier * @return */ private static String removeGenerics(String identifier) { StringBuilder b = new StringBuilder(); int genericCount = 0; for (int i = 0; i < identifier.length(); i++) { char ch = identifier.charAt(i); if (ch == '<') { genericCount++; continue; } if (ch == '>') { genericCount--; continue; } if (genericCount == 0) { b.append(ch); } } return b.toString(); } private static Class getClassFromName(String className) throws ClassNotFoundException { return ClassUtils.forCanonicalName(className, false, CheckOverrides.class.getClassLoader()); } private static void setup() { if (methods == null) { methods = new HashMap<>(); List<ClassMirror<?>> classes = ClassDiscovery.getDefaultInstance().getKnownClasses(ClassDiscovery.GetClassContainer(CheckOverrides.class)); for (ClassMirror cm : classes) { Class c = cm.loadClass(CheckOverrides.class.getClassLoader(), false); if (c.isInterface()) { continue; } Set<Method> mm = getPotentiallyOverridingMethods(c); if (!mm.isEmpty()) { methods.put(c, mm); } } } } /** * Returns a list of potentially overriding methods in a class. That is, the * non-private, non-static methods. * * @param c * @return */ private static Set<Method> getPotentiallyOverridingMethods(Class c) { Set<Method> methodList = new HashSet<>(); for (Method m : c.getDeclaredMethods()) { //Ignore static or public methods, since those can't override anything if ((m.getModifiers() & Modifier.PRIVATE) == 0 && (m.getModifiers() & Modifier.STATIC) == 0 && !m.isSynthetic()) { methodList.add(m); } } return methodList; } /** * Returns a list of overridable methods in a class. This includes * non-static, non-private, non-final methods. * * @param c * @return */ private static List<Method> getOverridableMethods(Class c) { List<Method> methodList = new ArrayList<>(); for (Method m : c.getDeclaredMethods()) { if ((m.getModifiers() & Modifier.PRIVATE) == 0 && (m.getModifiers() & Modifier.STATIC) == 0 && (m.getModifiers() & Modifier.FINAL) == 0 && !m.isSynthetic()) { methodList.add(m); } } return methodList; } }