/*
* Contributions to FindBugs
* Copyright (C) 2009, Andrei Loskutov
*
* The original code was developed by Andrei Loskutov under the BSD lizense for the
* Bytecode Outline plugin at http://andrei.gmxhome.de/bytecode/index.html
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package de.tobject.findbugs.reporter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import de.tobject.findbugs.FindbugsPlugin;
/**
* Utility to find right JDT Java types for anonymous classes
*
* @author Andrei
*/
public class JdtUtils {
static class AnonymClassComparator implements Comparator<IType> {
private final IType topAncestorType;
private final SourceOffsetComparator sourceComparator;
private final boolean is50OrHigher;
private final Map/* <IJavaElement,Integer> */<IType, Integer> map;
/**
* @param javaElement
* @param sourceComparator
*/
public AnonymClassComparator(IType javaElement, SourceOffsetComparator sourceComparator) {
this.sourceComparator = sourceComparator;
is50OrHigher = is50OrHigher(javaElement);
topAncestorType = (IType) getLastAncestor(javaElement, IJavaElement.TYPE);
map = new IdentityHashMap<IType, Integer>();
}
/**
* Very simple comparison based on init/not init block decision and then
* on the source code position
*/
private int compare50(IType m1, IType m2) {
IJavaElement firstAncestor1 = getFirstAncestor(m1);
IJavaElement firstAncestor2 = getFirstAncestor(m2);
int compilePrio1 = getCompilePrio(m1, firstAncestor1);
int compilePrio2 = getCompilePrio(m2, firstAncestor2);
if (compilePrio1 > compilePrio2) {
return -1;
} else if (compilePrio1 < compilePrio2) {
return 1;
} else {
return sourceComparator.compare(m1, m2);
}
}
/**
* If "deep" is the same, then source order win. 1) from instance init
* 2) from deepest inner from instance init (deepest first) 3) from
* static init 4) from deepest inner from static init (deepest first) 5)
* from deepest inner (deepest first) 7) regular anon classes from main
* class
*
* <br>
* Note, that nested inner anon. classes which do not have different
* non-anon. inner class ancestors, are compiled in they nesting order,
* opposite to rule 2)
*
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(IType m1, IType m2) {
if (m1 == m2) {
return 0;
}
if (is50OrHigher) {
return compare50(m1, m2);
}
IJavaElement firstAncestor1 = getFirstAncestor(m1);
IJavaElement firstAncestor2 = getFirstAncestor(m2);
int compilePrio1 = getCompilePrio(m1, firstAncestor1);
int compilePrio2 = getCompilePrio(m2, firstAncestor2);
if (compilePrio1 > compilePrio2) {
return -1;
} else if (compilePrio1 < compilePrio2) {
return 1;
} else {
firstAncestor1 = getFirstNonAnonymous(m1, topAncestorType);
firstAncestor2 = getFirstNonAnonymous(m2, topAncestorType);
if (firstAncestor1 == firstAncestor2) {
if (isLocal(firstAncestor1)) {
// we have to sort init blocks in local classes before
// other local class methods
// search for initializer block
boolean fromInitBlock1 = isFromInitBlock(m1);
boolean fromInitBlock2 = isFromInitBlock(m2);
if (fromInitBlock1 ^ fromInitBlock2) {
return fromInitBlock1 ? -1 : 1;
}
}
return sourceComparator.compare(m1, m2);
}
boolean isLocal = isLocal(firstAncestor1) || isLocal(firstAncestor2);
if (isLocal) {
return sourceComparator.compare(m1, m2);
}
/*
* for anonymous classes which have first non-common
* non-anonymous ancestor, the order is the reversed definition
* order
*/
int topAncestorDistance1 = getTopAncestorDistance(firstAncestor1, topAncestorType);
int topAncestorDistance2 = getTopAncestorDistance(firstAncestor2, topAncestorType);
if (topAncestorDistance1 > topAncestorDistance2) {
return -1;
} else if (topAncestorDistance1 < topAncestorDistance2) {
return 1;
} else {
return sourceComparator.compare(m1, m2);
}
}
}
private int getCompilePrio(IType anonType, IJavaElement firstAncestor) {
int compilePrio;
Integer prio;
if ((prio = map.get(anonType)) != null) {
compilePrio = prio.intValue();
if (Reporter.DEBUG) {
System.out.println("Using cache");
}
} else {
compilePrio = getAnonCompilePriority(anonType, firstAncestor, topAncestorType, is50OrHigher);
map.put(anonType, Integer.valueOf(compilePrio));
if (Reporter.DEBUG) {
System.out.println("Calculating value!");
}
}
return compilePrio;
}
}
static class SourceOffsetComparator implements Comparator<IType> {
/**
* First source occurrence wins.
*
* @param o1
* should be IType
* @param o2
* should be IType
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(IType o1, IType o2) {
IType m1 = o1;
IType m2 = o2;
int idx1, idx2;
try {
ISourceRange sr1 = m1.getSourceRange();
ISourceRange sr2 = m2.getSourceRange();
if (sr1 == null || sr2 == null) {
return 0;
}
idx1 = sr1.getOffset();
idx2 = sr2.getOffset();
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "SourceOffsetComparator failed");
return 0;
}
return idx1 - idx2;
}
}
/**
* @param javaElt
* @return true, if corresponding java project has compiler setting to
* generate bytecode for jdk 1.5 and above
*/
private static boolean is50OrHigher(IJavaElement javaElt) {
IJavaProject project = javaElt.getJavaProject();
String option = project.getOption(JavaCore.COMPILER_COMPLIANCE, true);
boolean result = JavaCore.VERSION_1_5.equals(option);
if (result) {
return result;
}
// probably > 1.5?
result = JavaCore.VERSION_1_4.equals(option);
if (result) {
return false;
}
result = JavaCore.VERSION_1_3.equals(option);
if (result) {
return false;
}
result = JavaCore.VERSION_1_2.equals(option);
if (result) {
return false;
}
result = JavaCore.VERSION_1_1.equals(option);
if (result) {
return false;
}
// unknown = > 1.5
return true;
}
/**
* Get the anonymous inner class with given parent type and class number
* (like Hello$5.class)
*
* @param parentType
* the parent of anon. type
* @return may return null, if we cannot find such anonymous class
*/
public static IType findAnonymous(IType parentType, String name) {
for (int i = 0; i < name.length(); i++) {
if (!Character.isDigit(name.charAt(i))) {
return null;
}
}
// -1 because compiler starts generated classes always with 1
int anonIndex = Integer.parseInt(name) - 1;
if (anonIndex < 0) {
return null;
}
List<IType> list = new ArrayList<IType>();
/*
* For JDK >= 1.5 in Eclipse 3.1+ the naming schema for nested anonymous
* classes was changed from A$1, A$2, A$3, A$4, ..., A$n to A$1, A$1$1,
* A$1$2, A$1$2$1, ..., A$2, A$2$1, A$2$2, ..., A$x$y
*/
boolean allowNested = !is50OrHigher(parentType);
IParent declaringType;
if (allowNested) {
declaringType = (IType) getLastAncestor(parentType, IJavaElement.TYPE);
} else {
declaringType = parentType.getDeclaringType();
}
if (declaringType == null) {
declaringType = parentType;
}
try {
collectAllAnonymous(list, declaringType, allowNested);
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "collectAllAnonymous() failed");
}
if (list.size() <= anonIndex) {
return null;
}
sortAnonymous(list, parentType);
return list.get(anonIndex);
}
/**
* Traverses down the children tree of this parent and collect all child
* anon. classes
*
* @param list
* @param parent
* @param allowNested
* true to search in IType child elements too
* @throws JavaModelException
*/
private static void collectAllAnonymous(List<IType> list, IParent parent, boolean allowNested) throws JavaModelException {
IJavaElement[] children = parent.getChildren();
for (int i = 0; i < children.length; i++) {
IJavaElement childElem = children[i];
if (isAnonymousType(childElem)) {
list.add((IType) childElem);
}
if (childElem instanceof IParent) {
if (allowNested || !(childElem instanceof IType)) {
collectAllAnonymous(list, (IParent) childElem, allowNested);
}
}
}
}
/**
* Sort given anonymous classes in order like java compiler would generate
* output classes, in context of given anonymous type
*
* @param anonymous
*/
private static void sortAnonymous(List<IType> anonymous, IType anonType) {
SourceOffsetComparator sourceComparator = new SourceOffsetComparator();
final AnonymClassComparator classComparator = new AnonymClassComparator(anonType, sourceComparator);
Collections.sort(anonymous, classComparator);
if (Reporter.DEBUG) {
debugCompilePrio(classComparator);
}
}
private static void debugCompilePrio(final AnonymClassComparator classComparator) {
final Map<IType, Integer> map = classComparator.map;
Comparator<IType> prioComp = new Comparator<IType>() {
public int compare(IType e1, IType e2) {
int result = map.get(e1).compareTo(map.get(e2));
if (result == 0) {
return e1.toString().compareTo(e2.toString());
}
return -result;
}
};
List<IType> keys = new ArrayList<IType>(map.keySet());
Collections.sort(keys, prioComp);
for (Iterator<IType> iterator = keys.iterator(); iterator.hasNext();) {
Object key = iterator.next();
System.out.println(map.get(key) + " : " + key);
}
}
private static int getAnonCompilePriority(IJavaElement elt, IJavaElement firstAncestor, IJavaElement topAncestor,
boolean is50OrHigher) {
if (is50OrHigher) {
return getAnonCompilePriority50(elt, firstAncestor, topAncestor);
}
IJavaElement firstNonAnon = getFirstNonAnonymous(elt, topAncestor);
// get rid of children from local types
if (topAncestor != firstNonAnon && isLocal(firstNonAnon)) {
return 5; // local anon. types have same prio as anon. from regular
// code
}
IJavaElement initBlock = getLastAncestor(elt, IJavaElement.INITIALIZER);
// test is for anon. classes from initializer blocks
if (initBlock != null) {
if (isAnyParentLocal(firstAncestor, topAncestor)) {
return 5; // init blocks from local types have same prio as
// regular
}
if (firstAncestor == topAncestor) {
return 10; // instance init from top level type has top prio
}
if ( /* firstNonAnon != topAncestor && */!isStatic((IMember) firstNonAnon)) {
return 8; // init blocks from non static types have top 2 prio
}
return 7; // init blocks from static classes
}
if (firstNonAnon != topAncestor) {
if (!isStatic((IMember) firstNonAnon)) {
return 7; // children of member types first
}
return 6; // children of static types
}
// anon. types from "regular" code
return 5;
}
/**
* 1) from instance init 2) from deepest inner from instance init (deepest
* first) 3) from static init 4) from deepest inner from static init
* (deepest first) 5) from deepest inner (deepest first) 6) regular anon
* classes from main class
*
* <br>
* Note, that nested inner anon. classes which do not have different
* non-anon. inner class ancestors, are compiled in they nesting order,
* opposite to rule 2)
*
* @param javaElement
* @return priority - lesser mean wil be compiled later, a value > 0
*/
private static int getAnonCompilePriority50(IJavaElement javaElement, IJavaElement firstAncestor, IJavaElement topAncestor) {
// search for initializer block
IJavaElement initBlock = getLastAncestor(javaElement, IJavaElement.INITIALIZER);
// test is for anon. classes from initializer blocks
if (initBlock != null) {
return 10; // from inner from class init
}
// test for anon. classes from "regular" code
return 5;
}
/**
* @param javaElement
* @return null, if javaElement is top level class
*/
private static IType getFirstAncestor(IJavaElement javaElement) {
IJavaElement parent = javaElement;
if (javaElement.getElementType() == IJavaElement.TYPE) {
parent = javaElement.getParent();
}
if (parent != null) {
return (IType) parent.getAncestor(IJavaElement.TYPE);
}
return null;
}
/**
* @param javaElement
* @return first non-anonymous ancestor
*/
private static IJavaElement getFirstNonAnonymous(IJavaElement javaElement, IJavaElement topAncestor) {
if (javaElement.getElementType() == IJavaElement.TYPE && !isAnonymousType(javaElement)) {
return javaElement;
}
IJavaElement parent = javaElement.getParent();
if (parent == null) {
return topAncestor;
}
IJavaElement ancestor = parent.getAncestor(IJavaElement.TYPE);
if (ancestor != null) {
return getFirstNonAnonymous(ancestor, topAncestor);
}
return topAncestor;
}
private static IJavaElement getLastAncestor(IJavaElement javaElement, int elementType) {
IJavaElement lastFound = null;
if (elementType == javaElement.getElementType()) {
lastFound = javaElement;
}
IJavaElement parent = javaElement.getParent();
if (parent == null) {
return lastFound;
}
IJavaElement ancestor = parent.getAncestor(elementType);
if (ancestor != null) {
return getLastAncestor(ancestor, elementType);
}
return lastFound;
}
/**
* @param javaElement
* @return distance to given ancestor, 0 if it is the same, -1 if ancestor
* with type IJavaElement.TYPE does not exist
*/
private static int getTopAncestorDistance(IJavaElement javaElement, IJavaElement topAncestor) {
if (topAncestor == javaElement) {
return 0;
}
IJavaElement ancestor = getFirstAncestor(javaElement);
if (ancestor != null) {
return 1 + getTopAncestorDistance(ancestor, topAncestor);
}
// this is not possible, if ancestor exists - which return value should
// we use?
return -1;
}
/**
* @param javaElement
* @return true, if given element is anonymous inner class
*/
private static boolean isAnonymousType(IJavaElement javaElement) {
try {
return javaElement instanceof IType && ((IType) javaElement).isAnonymous();
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "isAnonymousType() failed");
}
return false;
}
/**
* @param type
* should be inner type.
* @return true, if given element is a type defined in the initializer block
*/
private static boolean isFromInitBlock(IType type) {
IJavaElement ancestor = type.getAncestor(IJavaElement.INITIALIZER);
return ancestor != null;
}
/**
* @param innerType
* should be inner type.
* @return true, if given element is inner class from initializer block or
* method body
*/
private static boolean isLocal(IJavaElement innerType) {
try {
return innerType instanceof IType && ((IType) innerType).isLocal();
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "isLocal() failed");
}
return false;
}
private static boolean isStatic(IMember firstNonAnon) {
int topFlags = 0;
try {
topFlags = firstNonAnon.getFlags();
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "isStatic() failed");
}
return Flags.isStatic(topFlags);
}
/**
* @param elt
* @return true, if given element is inner class from initializer block or
* method body
*/
private static boolean isAnyParentLocal(IJavaElement elt, IJavaElement topParent) {
if (isLocal(elt)) {
return true;
}
IJavaElement parent = elt.getParent();
while (parent != null && parent != topParent) {
if (isLocal(parent)) {
return true;
}
parent = parent.getParent();
}
return false;
}
}