com.googlecode.d2j.dex.Dex2Asm Maven / Gradle / Ivy
The newest version!
package com.googlecode.d2j.dex;
import java.util.*;
import com.googlecode.d2j.converter.Dex2IRConverter;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.InnerClassNode;
import com.googlecode.d2j.*;
import com.googlecode.d2j.converter.IR2JConverter;
import com.googlecode.d2j.node.*;
import com.googlecode.dex2jar.ir.IrMethod;
import com.googlecode.dex2jar.ir.ts.*;
import com.googlecode.dex2jar.ir.ts.array.FillArrayTransformer;
public class Dex2Asm {
protected static class Clz {
public int access;
public Clz enclosingClass;
public Method enclosingMethod;
public String innerName;
public Set inners = null;
public final String name;
public Clz(String name) {
super();
this.name = name;
}
void addInner(Clz clz) {
if (inners == null) {
inners = new HashSet();
}
inners.add(clz);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Clz other = (Clz) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
public String toString() {
return "" + name;
}
}
protected static final int ACC_INTERFACE_ABSTRACT = (Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT);
private static final int NO_CODE_MASK = DexConstants.ACC_ABSTRACT | DexConstants.ACC_NATIVE
| DexConstants.ACC_ANNOTATION;
protected static final CleanLabel T_cleanLabel = new CleanLabel();
protected static final EndRemover T_endRemove = new EndRemover();
protected static final Ir2JRegAssignTransformer T_ir2jRegAssign = new Ir2JRegAssignTransformer();
protected static final NewTransformer T_new = new NewTransformer();
protected static final RemoveConstantFromSSA T_removeConst = new RemoveConstantFromSSA();
protected static final RemoveLocalFromSSA T_removeLocal = new RemoveLocalFromSSA();
protected static final ExceptionHandlerTrim T_trimEx = new ExceptionHandlerTrim();
protected static final TypeTransformer T_type = new TypeTransformer();
// protected static final TopologicalSort T_topologicalSort = new TopologicalSort();
protected static final DeadCodeTransformer T_deadCode = new DeadCodeTransformer();
protected static final FillArrayTransformer T_fillArray = new FillArrayTransformer();
protected static final AggTransformer T_agg = new AggTransformer();
protected static final UnSSATransformer T_unssa = new UnSSATransformer();
protected static final ZeroTransformer T_zero = new ZeroTransformer();
protected static final VoidInvokeTransformer T_voidInvoke = new VoidInvokeTransformer();
protected static final NpeTransformer T_npe = new NpeTransformer();
protected static final MultiArrayTransformer T_multiArray = new MultiArrayTransformer();
static private int clearClassAccess(boolean isInner, int access) {
if ((access & Opcodes.ACC_INTERFACE) == 0) { // issue 55
access |= Opcodes.ACC_SUPER;// 解决生成的class文件使用dx重新转换时使用的指令与原始指令不同的问题
}
// access in class has no acc_static or acc_private
access &= ~(Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE);
if (isInner && (access & Opcodes.ACC_PROTECTED) != 0) {// protected inner class are public
access &= ~Opcodes.ACC_PROTECTED;
access |= Opcodes.ACC_PUBLIC;
}
access &= ~DexConstants.ACC_DECLARED_SYNCHRONIZED; // clean ACC_DECLARED_SYNCHRONIZED
return access;
}
static private int clearInnerAccess(int access) {
access &= (~Opcodes.ACC_SUPER);// inner class attr has no acc_super
if (0 != (access & Opcodes.ACC_PRIVATE)) {// clear public/protected if it is private
access &= ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED);
} else if (0 != (access & Opcodes.ACC_PROTECTED)) {// clear public if it is protected
access &= ~(Opcodes.ACC_PUBLIC);
}
return access;
}
protected static String toInternalName(DexType type) {
return toInternalName(type.desc);
}
protected static String toInternalName(String desc) {
// TODO without creating object
return Type.getType(desc).getInternalName();
}
public static void accept(DexAnnotationNode ann, ClassVisitor v) {
AnnotationVisitor av = v.visitAnnotation(ann.type, ann.visibility != Visibility.BUILD);
if (av != null) {
accept(ann.items, av);
av.visitEnd();
}
}
public static void accept(List anns, ClassVisitor cv) {
if (anns != null) {
for (DexAnnotationNode ann : anns) {
if (ann.visibility != Visibility.SYSTEM) {
accept(ann, cv);
}
}
}
}
public static void accept(List anns, FieldVisitor fv) {
if (anns != null) {
for (DexAnnotationNode ann : anns) {
if (ann.visibility != Visibility.SYSTEM) {
accept(ann, fv);
}
}
}
}
public static void accept(List anns, MethodVisitor mv) {
if (anns != null) {
for (DexAnnotationNode ann : anns) {
if (ann.visibility != Visibility.SYSTEM) {
accept(ann, mv);
}
}
}
}
public static void accept(DexAnnotationNode ann, MethodVisitor v) {
AnnotationVisitor av = v.visitAnnotation(ann.type, ann.visibility != Visibility.BUILD);
if (av != null) {
accept(ann.items, av);
av.visitEnd();
}
}
public static void acceptParameter(DexAnnotationNode ann, int index, MethodVisitor v) {
AnnotationVisitor av = v.visitParameterAnnotation(index, ann.type, ann.visibility != Visibility.BUILD);
if (av != null) {
accept(ann.items, av);
av.visitEnd();
}
}
public static void accept(DexAnnotationNode ann, FieldVisitor v) {
AnnotationVisitor av = v.visitAnnotation(ann.type, ann.visibility != Visibility.BUILD);
if (av != null) {
accept(ann.items, av);
av.visitEnd();
}
}
public static void accept(List items, AnnotationVisitor av) {
for (DexAnnotationNode.Item item : items) {
accept(av, item.name, item.value);
}
}
private static void accept(AnnotationVisitor dav, String name, Object o) {
if (o instanceof Object[]) {
AnnotationVisitor arrayVisitor = dav.visitArray(name);
if (arrayVisitor != null) {
Object[] array = (Object[]) o;
for (Object e : array) {
accept(arrayVisitor, null, e);
}
arrayVisitor.visitEnd();
}
} else if (o instanceof DexAnnotationNode) {
DexAnnotationNode ann = (DexAnnotationNode) o;
AnnotationVisitor av = dav.visitAnnotation(name, ann.type);
if (av != null) {
for (DexAnnotationNode.Item item : ann.items) {
accept(av, item.name, item.value);
}
av.visitEnd();
}
} else if (o instanceof Field) {
Field f = (Field) o;
dav.visitEnum(name, f.getType(), f.getName());
} else if (o instanceof DexType) {
dav.visit(name, Type.getType(((DexType) o).desc));
} else if (o instanceof Method) {
System.err.println("WARN: ignored method annotation value");
} else {
if (o == null) {
System.err.println("WARN: ignored null annotation value");
} else {
dav.visit(name, o);
}
}
}
private static MethodVisitor collectBasicMethodInfo(DexMethodNode methodNode, ClassVisitor cv) {
String xthrows[] = null;
String signature = null;
if (methodNode.anns != null) {
for (DexAnnotationNode ann : methodNode.anns) {
if (ann.visibility == Visibility.SYSTEM) {
switch (ann.type) {
case DexConstants.ANNOTATION_THROWS_TYPE: {
Object[] strs = (Object[]) findAnnotationAttribute(ann, "value");
if (strs != null) {
xthrows = new String[strs.length];
for (int i = 0; i < strs.length; i++) {
DexType type = (DexType) strs[i];
xthrows[i] = toInternalName(type);
}
}
}
break;
case DexConstants.ANNOTATION_SIGNATURE_TYPE: {
Object[] strs = (Object[]) findAnnotationAttribute(ann, "value");
if (strs != null) {
StringBuilder sb = new StringBuilder();
for (Object str : strs) {
sb.append(str);
}
signature = sb.toString();
}
}
break;
}
}
}
}
int access = methodNode.access;
// clear ACC_DECLARED_SYNCHRONIZED and ACC_CONSTRUCTOR from method flags
final int cleanFlag = ~((DexConstants.ACC_DECLARED_SYNCHRONIZED | DexConstants.ACC_CONSTRUCTOR));
access &= cleanFlag;
return cv.visitMethod(access, methodNode.method.getName(), methodNode.method.getDesc(), signature, xthrows);
}
protected static Map collectClzInfo(DexFileNode fileNode) {
Map classes = new HashMap<>();
for (DexClassNode classNode : fileNode.clzs) {
Clz clz = get(classes, classNode.className);
clz.access = (clz.access & ~ACC_INTERFACE_ABSTRACT) | classNode.access;
if (classNode.anns != null) {
for (DexAnnotationNode ann : classNode.anns) {
if (ann.visibility == Visibility.SYSTEM) {
switch (ann.type) {
case DexConstants.ANNOTATION_ENCLOSING_CLASS_TYPE: {
DexType type = (DexType) findAnnotationAttribute(ann, "value");
Clz enclosingClass = get(classes, type.desc);
clz.enclosingClass = enclosingClass;
// apply patch from ChaeHoon Lim,
// obfuscated code may declare itself as enclosing class
// which cause dex2jar to endless loop
//if(!clz.name.equals(clz.enclosingClass.name)) {
// enclosingClass.addInner(clz);
//}
enclosingClass.addInner(clz);
}
break;
case DexConstants.ANNOTATION_ENCLOSING_METHOD_TYPE: {
Method m = (Method) findAnnotationAttribute(ann, "value");
Clz enclosingClass = get(classes, m.getOwner());
clz.enclosingClass = enclosingClass;
clz.enclosingMethod = m;
enclosingClass.addInner(clz);
}
break;
case DexConstants.ANNOTATION_INNER_CLASS_TYPE: {
for (DexAnnotationNode.Item it : ann.items) {
if ("accessFlags".equals(it.name)) {
clz.access |= (Integer) it.value & ~ACC_INTERFACE_ABSTRACT;
} else if ("name".equals(it.name)) {
clz.innerName = (String) it.value;
}
}
}
break;
case DexConstants.ANNOTATION_MEMBER_CLASSES_TYPE: {
Object ts[] = (Object[]) findAnnotationAttribute(ann, "value");
for (Object v : ts) {
DexType type = (DexType) v;
Clz inner = get(classes, type.desc);
clz.addInner(inner);
inner.enclosingClass = clz;
}
}
break;
}
}
}
}
}
return classes;
}
public void convertClass(DexClassNode classNode, ClassVisitorFactory cvf, DexFileNode fileNode) {
convertClass(fileNode.dexVersion, classNode, cvf, collectClzInfo(fileNode));
}
public void convertClass(DexClassNode classNode, ClassVisitorFactory cvf) {
convertClass(DexConstants.DEX_035, classNode, cvf);
}
public void convertClass(int dexVersion, DexClassNode classNode, ClassVisitorFactory cvf) {
convertClass(dexVersion, classNode, cvf, new HashMap());
}
private static boolean isJavaIdentifier(String str) {
if (str.length() < 1) {
return false;
}
if (!Character.isJavaIdentifierStart(str.charAt(0))) {
return false;
}
for (int i = 1; i < str.length(); i++) {
if (!Character.isJavaIdentifierPart(str.charAt(i))) {
return false;
}
}
return true;
}
public void convertClass(DexClassNode classNode, ClassVisitorFactory cvf, Map classes) {
convertClass(DexConstants.DEX_035, classNode, cvf, classes);
}
public void convertClass(DexFileNode dfn, DexClassNode classNode, ClassVisitorFactory cvf, Map classes) {
convertClass(dfn.dexVersion, classNode, cvf, classes);
}
public void convertClass(int dexVersion, DexClassNode classNode, ClassVisitorFactory cvf, Map classes) {
ClassVisitor cv = cvf.create(toInternalName(classNode.className));
if (cv == null) {
return;
}
// the default value of static-final field are omitted by dex, fix it
DexFix.fixStaticFinalFieldValue(classNode);
String signature = null;
if (classNode.anns != null) {
for (DexAnnotationNode ann : classNode.anns) {
if (ann.visibility == Visibility.SYSTEM) {
switch (ann.type) {
case DexConstants.ANNOTATION_SIGNATURE_TYPE: {
Object[] strs = (Object[]) findAnnotationAttribute(ann, "value");
if (strs != null) {
StringBuilder sb = new StringBuilder();
for (Object str : strs) {
sb.append(str);
}
signature = sb.toString();
}
}
break;
}
}
}
}
String interfaceInterNames[] = null;
if (classNode.interfaceNames != null) {
interfaceInterNames = new String[classNode.interfaceNames.length];
for (int i = 0; i < classNode.interfaceNames.length; i++) {
interfaceInterNames[i] = toInternalName(classNode.interfaceNames[i]);
}
}
Clz clzInfo = classes.get(classNode.className);
int access = classNode.access;
boolean isInnerClass = false;
if (clzInfo != null) {
isInnerClass = clzInfo.enclosingClass != null || clzInfo.enclosingMethod != null;
}
access = clearClassAccess(isInnerClass, access);
int version = dexVersion >= DexConstants.DEX_037 ? Opcodes.V1_8 : Opcodes.V1_6;
cv.visit(version, access, toInternalName(classNode.className), signature,
classNode.superClass == null ? null : toInternalName(classNode.superClass), interfaceInterNames);
List innerClassNodes = new ArrayList(5);
if (clzInfo != null) {
searchInnerClass(clzInfo, innerClassNodes, classNode.className);
}
if (isInnerClass) {
// build Outer Clz
if (clzInfo.innerName == null) {// anonymous Innerclass
Method enclosingMethod = clzInfo.enclosingMethod;
if (enclosingMethod != null) {
cv.visitOuterClass(toInternalName(enclosingMethod.getOwner()), enclosingMethod.getName(),
enclosingMethod.getDesc());
} else {
Clz enclosingClass = clzInfo.enclosingClass;
cv.visitOuterClass(toInternalName(enclosingClass.name), null, null);
}
}
searchEnclosing(clzInfo, innerClassNodes);
}
Collections.sort(innerClassNodes, INNER_CLASS_NODE_COMPARATOR);
for (InnerClassNode icn : innerClassNodes) {
if (icn.innerName != null && !isJavaIdentifier(icn.innerName)) {
System.err.println("WARN: ignored invalid inner class name " + ", treat as anonymous inner class.");
icn.innerName = null;
icn.outerName = null;
}
icn.accept(cv);
}
accept(classNode.anns, cv);
if (classNode.fields != null) {
for (DexFieldNode fieldNode : classNode.fields) {
convertField(classNode, fieldNode, cv);
}
}
if (classNode.methods != null) {
for (DexMethodNode methodNode : classNode.methods) {
convertMethod(classNode, methodNode, cv);
}
}
cv.visitEnd();
}
public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
IrMethod irMethod = dex2ir(methodNode);
optimize(irMethod);
ir2j(irMethod, mv);
}
public void convertDex(DexFileNode fileNode, ClassVisitorFactory cvf) {
if (fileNode.clzs != null) {
Map classes = collectClzInfo(fileNode);
for (DexClassNode classNode : fileNode.clzs) {
convertClass(fileNode, classNode, cvf, classes);
}
}
}
public void convertField(DexClassNode classNode, DexFieldNode fieldNode, ClassVisitor cv) {
String signature = null;
if (fieldNode.anns != null) {
for (DexAnnotationNode ann : fieldNode.anns) {
if (ann.visibility == Visibility.SYSTEM) {
switch (ann.type) {
case DexConstants.ANNOTATION_SIGNATURE_TYPE: {
Object[] strs = (Object[]) findAnnotationAttribute(ann, "value");
if (strs != null) {
StringBuilder sb = new StringBuilder();
for (Object str : strs) {
sb.append(str);
}
signature = sb.toString();
}
}
break;
}
}
}
}
Object value = convertConstantValue(fieldNode.cst);
final int FieldCleanFlag = ~DexConstants.ACC_DECLARED_SYNCHRONIZED;
FieldVisitor fv = cv.visitField(fieldNode.access & FieldCleanFlag, fieldNode.field.getName(),
fieldNode.field.getType(), signature, value);
if (fv == null) {
return;
}
accept(fieldNode.anns, fv);
fv.visitEnd();
}
public static Object[] convertConstantValues(Object[] v) {
Object[] copy = Arrays.copyOf(v, v.length);
for (int i = 0; i < copy.length; i++) {
Object ele = copy[i];
ele = convertConstantValue(ele);
copy[i] = ele;
}
return copy;
}
public static Object convertConstantValue(Object ele) {
if (ele instanceof DexType) {
ele = Type.getType(((DexType) ele).desc);
} else if (ele instanceof MethodHandle) {
Handle h = null;
MethodHandle mh = (MethodHandle) ele;
switch (mh.getType()) {
case MethodHandle.INSTANCE_GET:
h = new Handle(Opcodes.H_GETFIELD, toInternalName(mh.getField().getOwner()), mh.getField().getName(), mh.getField().getType());
break;
case MethodHandle.INSTANCE_PUT:
h = new Handle(Opcodes.H_PUTFIELD, toInternalName(mh.getField().getOwner()), mh.getField().getName(), mh.getField().getType());
break;
case MethodHandle.STATIC_GET:
h = new Handle(Opcodes.H_GETFIELD, toInternalName(mh.getField().getOwner()), mh.getField().getName(), mh.getField().getType());
break;
case MethodHandle.STATIC_PUT:
h = new Handle(Opcodes.H_PUTFIELD, toInternalName(mh.getField().getOwner()), mh.getField().getName(), mh.getField().getType());
break;
case MethodHandle.INVOKE_INSTANCE:
h = new Handle(Opcodes.H_INVOKEVIRTUAL, toInternalName(mh.getMethod().getOwner()), mh.getMethod().getName(), mh.getMethod().getDesc());
break;
case MethodHandle.INVOKE_STATIC:
h = new Handle(Opcodes.H_INVOKESTATIC, toInternalName(mh.getMethod().getOwner()), mh.getMethod().getName(), mh.getMethod().getDesc());
break;
case MethodHandle.INVOKE_CONSTRUCTOR:
h = new Handle(Opcodes.H_NEWINVOKESPECIAL, toInternalName(mh.getMethod().getOwner()), mh.getMethod().getName(), mh.getMethod().getDesc());
break;
case MethodHandle.INVOKE_DIRECT:
h = new Handle(Opcodes.H_INVOKESPECIAL, toInternalName(mh.getMethod().getOwner()), mh.getMethod().getName(), mh.getMethod().getDesc());
break;
case MethodHandle.INVOKE_INTERFACE:
h = new Handle(Opcodes.H_INVOKEINTERFACE, toInternalName(mh.getMethod().getOwner()), mh.getMethod().getName(), mh.getMethod().getDesc());
break;
}
ele = h;
} else if (ele instanceof Proto) {
ele = Type.getMethodType(((Proto) ele).getDesc());
}
return ele;
}
public void convertMethod(DexClassNode classNode, DexMethodNode methodNode, ClassVisitor cv) {
MethodVisitor mv = collectBasicMethodInfo(methodNode, cv);
if (mv == null) {
return;
}
if (0 != (classNode.access & DexConstants.ACC_ANNOTATION)) { // its inside an annotation
Object defaultValue = null;
if (classNode.anns != null) {
for (DexAnnotationNode ann : classNode.anns) {
if (ann.visibility == Visibility.SYSTEM && ann.type.equals(DexConstants.ANNOTATION_DEFAULT_TYPE)) {
DexAnnotationNode node = (DexAnnotationNode) findAnnotationAttribute(ann, "value");
if (node != null) {
defaultValue = findAnnotationAttribute(node, methodNode.method.getName());
}
break;
}
}
}
if (defaultValue != null) {
AnnotationVisitor av = mv.visitAnnotationDefault();
if (av != null) {
accept(av, null, defaultValue);
av.visitEnd();
}
}
}
accept(methodNode.anns, mv);
if (methodNode.parameterAnns != null) {
for (int i = 0; i < methodNode.parameterAnns.length; i++) {
List anns = methodNode.parameterAnns[i];
if (anns != null) {
for (DexAnnotationNode ann : anns) {
if (ann.visibility != Visibility.SYSTEM) {
acceptParameter(ann, i, mv);
}
}
}
}
}
if ((NO_CODE_MASK & methodNode.access) == 0) { // has code
if (methodNode.codeNode != null) {
mv.visitCode();
convertCode(methodNode, mv);
}
}
mv.visitEnd();
}
public IrMethod dex2ir(DexMethodNode methodNode) {
return new Dex2IRConverter()
.convert(0 != (methodNode.access & DexConstants.ACC_STATIC), methodNode.method, methodNode.codeNode);
}
protected static Object findAnnotationAttribute(DexAnnotationNode ann, String name) {
for (DexAnnotationNode.Item item : ann.items) {
if (item.name.equals(name)) {
return item.value;
}
}
return null;
}
private static Clz get(Map classes, String name) {
Clz clz = classes.get(name);
if (clz == null) {
clz = new Clz(name);
classes.put(name, clz);
}
return clz;
}
public void ir2j(IrMethod irMethod, MethodVisitor mv) {
new IR2JConverter(false).convert(irMethod, mv);
mv.visitMaxs(-1, -1);
}
public void optimize(IrMethod irMethod) {
T_cleanLabel.transform(irMethod);
T_deadCode.transform(irMethod);
T_removeLocal.transform(irMethod);
T_removeConst.transform(irMethod);
T_zero.transform(irMethod);
if (T_npe.transformReportChanged(irMethod)) {
T_deadCode.transform(irMethod);
T_removeLocal.transform(irMethod);
T_removeConst.transform(irMethod);
}
T_new.transform(irMethod);
T_fillArray.transform(irMethod);
T_agg.transform(irMethod);
T_multiArray.transform(irMethod);
T_voidInvoke.transform(irMethod);
T_type.transform(irMethod);
T_unssa.transform(irMethod);
T_trimEx.transform(irMethod);
T_ir2jRegAssign.transform(irMethod);
}
/**
* For structure
*
*
* class A {
* class B {
* class WeAreHere {
* }
* }
* }
*
*
* this method will add
*
*
* InnerClass Outter
* A$B$WeAreHere A$B
* A$B A
*
*
* to WeAreHere.class
*
*/
private static void searchEnclosing(Clz clz, List innerClassNodes) {
Set visitedClz = new HashSet<>();
for (Clz p = clz; p != null; p = p.enclosingClass) {
if (!visitedClz.add(p)) { // prevent endless loop
break;
}
Clz enclosingClass = p.enclosingClass;
if (enclosingClass == null) {
break;
}
if (enclosingClass == clz) {
// enclosing itself, that is impossible
break;
}
int accessInInner = clearInnerAccess(p.access);
if (p.innerName != null) {// non-anonymous Innerclass
innerClassNodes.add(new InnerClassNode(toInternalName(p.name),
toInternalName(enclosingClass.name), p.innerName, accessInInner));
} else {// anonymous Innerclass
innerClassNodes.add(new InnerClassNode(toInternalName(p.name), null, null, accessInInner));
}
}
}
/**
* For structure
*
*
* class WeAreHere {
* class A {
* class B {
*
* }
* }
* }
*
*
* this method will add
*
*
* InnerClass Outter
* WeAreHere$A$B WeAreHere$A
* WeAreHere$A WeAreHere
*
*
* to WeAreHere.class
*
* @param clz
*/
private static void searchInnerClass(Clz clz, List innerClassNodes, String className) {
Set visited = new HashSet<>();
Stack stack = new Stack<>();
stack.push(clz);
while (!stack.empty()) {
clz = stack.pop();
if (visited.contains(clz)) {
continue;
} else {
visited.add(clz);
}
if (clz.inners != null) {
for (Clz inner : clz.inners) {
if (inner.innerName == null) {// anonymous Innerclass
innerClassNodes.add(new InnerClassNode(toInternalName(inner.name), null, null,
clearInnerAccess(inner.access)));
} else {// non-anonymous Innerclass
innerClassNodes.add(new InnerClassNode(toInternalName(inner.name), toInternalName(className),
inner.innerName, clearInnerAccess(inner.access)));
}
stack.push(inner);
}
}
}
}
private static final Comparator INNER_CLASS_NODE_COMPARATOR = new Comparator() {
@Override
public int compare(InnerClassNode o1, InnerClassNode o2) {
return o1.name.compareTo(o2.name);
}
};
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy