org.springsource.loaded.TypeDiffComputer.java Source code

Java tutorial

Introduction

Here is the source code for org.springsource.loaded.TypeDiffComputer.java

Source

/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springsource.loaded;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.MultiANewArrayInsnNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;

/**
 * Compute the differences between two versions of a type as a series of deltas. Entry point is the computeDifferences method.
 * 
 * @author Andy Clement
 * @since 0.5.0
 */
public class TypeDiffComputer implements Opcodes {

    public static TypeDelta computeDifferences(byte[] oldbytes, byte[] newbytes) {
        ClassNode oldClassNode = new ClassNode();
        new ClassReader(oldbytes).accept(oldClassNode, 0);
        ClassNode newClassNode = new ClassNode();
        new ClassReader(newbytes).accept(newClassNode, 0);
        TypeDelta delta = computeDelta(oldClassNode, newClassNode);
        return delta;
    }

    private static TypeDelta computeDelta(ClassNode oldClassNode, ClassNode newClassNode) {
        // The type itself: (int version, int access, String name, String signature, String superName, String[] interfaces) {
        TypeDelta td = new TypeDelta();
        computeTypeDelta(oldClassNode, newClassNode, td);
        computeFieldDelta(oldClassNode, newClassNode, td);
        computeMethodDelta(oldClassNode, newClassNode, td);
        // TODO delta: implement the rest of computeDelta.  These methods from ClassVisitor should help in knowing what is left to do:
        //      public void visitSource(String source, String debug) {
        //      public void visitOuterClass(String owner, String name, String desc) {
        //      public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        //      public void visitAttribute(Attribute attr) {
        //      public void visitInnerClass(String name, String outerName, String innerName, int access) {
        //      public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        //      public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        //      public void visitEnd() {
        return td;
    }

    @SuppressWarnings("unchecked")
    private static void computeMethodDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
        List<MethodNode> nMethods = newClassNode.methods;
        List<MethodNode> oMethods = new ArrayList<MethodNode>(oldClassNode.methods);

        // Going through the new methods and comparing them to the old
        if (nMethods != null) {
            for (MethodNode nMethod : nMethods) {
                MethodNode found = null;
                for (MethodNode oMethod : oMethods) {
                    if (oMethod.name.equals(nMethod.name) && oMethod.desc.equals(nMethod.desc)) { // TODO modifiers compared?
                        found = oMethod;
                        computeAnyMethodDifferences(oMethod, nMethod, td);
                    }
                }
                if (found == null) {
                    td.addNewMethod(nMethod);
                } else {
                    oMethods.remove(found);
                }
            }
        }
        for (MethodNode lostMethod : oMethods) {
            td.addLostMethod(lostMethod);
        }
    }

    @SuppressWarnings("unchecked")
    private static void computeFieldDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
        //      int oSize = oldClassNode.fields.size();
        int nSize = newClassNode.fields.size();

        // Take a copy as we are going to delete entries in the next loop
        List<FieldNode> oFields = new ArrayList<FieldNode>(oldClassNode.fields);

        // Going through the new fields comparing them to the old
        for (int n = 0; n < nSize; n++) {
            FieldNode nField = (FieldNode) newClassNode.fields.get(n);
            FieldNode found = null;
            for (FieldNode oField : oFields) {
                if (oField.name.equals(nField.name)) {
                    // found it!
                    found = oField;
                    // is it exactly the same?
                    computeAnyFieldDifferences(oField, nField, td);
                }
            }
            if (found == null) {
                // this is a new field
                td.addNewField(nField);
            } else {
                oFields.remove(found);
            }
        }

        // Those left in oFields were not in nFields so have been removed!
        for (FieldNode lostField : oFields) {
            td.addLostField(lostField);
        }
    }

    /**
     * Check the properties of the field - if they have changed at all then record what kind of change for the field. Thinking the
     * type delta should have a map from names to a delta describing (capturing) the change.
     */
    @SuppressWarnings("unchecked")
    private static void computeAnyFieldDifferences(FieldNode oField, FieldNode nField, TypeDelta td) {
        // Want to record things that are different between these two fields...
        FieldDelta fd = new FieldDelta(oField.name);
        if (oField.access != nField.access) {
            // access changed
            fd.setAccessChanged(oField.access, nField.access);
        }
        if (!oField.desc.equals(nField.desc)) {
            // type changed
            fd.setTypeChanged(oField.desc, nField.desc);
        }
        String annotationChange = compareAnnotations(oField.invisibleAnnotations, nField.invisibleAnnotations);
        annotationChange = annotationChange
                + compareAnnotations(oField.visibleAnnotations, nField.visibleAnnotations);
        if (annotationChange.length() != 0) {
            fd.setAnnotationsChanged(annotationChange);
        }
        if (fd.hasAnyChanges()) {
            // it needs recording
            td.addChangedField(fd);
        }
    }

    /**
     * Determine if there any differences between the methods supplied. A MethodDelta object is built to record any differences and
     * stored against the type delta.
     * 
     * @param oMethod 'old' method
     * @param nMethod 'new' method
     * @param td the type delta where changes are currently being accumulated
     */
    private static void computeAnyMethodDifferences(MethodNode oMethod, MethodNode nMethod, TypeDelta td) {
        MethodDelta md = new MethodDelta(oMethod.name, oMethod.desc);
        if (oMethod.access != nMethod.access) {
            md.setAccessChanged(oMethod.access, nMethod.access);
        }
        // TODO annotations
        InsnList oInstructions = oMethod.instructions;
        InsnList nInstructions = nMethod.instructions;
        if (oInstructions.size() != nInstructions.size()) {
            md.setInstructionsChanged(oInstructions.toArray(), nInstructions.toArray());
        } else {
            // TODO Just interested in constructors right now - should add others
            if (oMethod.name.charAt(0) == '<') {
                String oInvokeSpecialDescriptor = null;
                String nInvokeSpecialDescriptor = null;
                int oUninitCount = 0;
                int nUninitCount = 0;
                boolean codeChange = false;
                for (int i = 0, max = oInstructions.size(); i < max; i++) {
                    AbstractInsnNode oInstruction = oInstructions.get(i);
                    AbstractInsnNode nInstruction = nInstructions.get(i);
                    if (!codeChange) {
                        if (!sameInstruction(oInstruction, nInstruction)) {
                            codeChange = true;
                        }

                    }
                    if (oInstruction.getType() == AbstractInsnNode.TYPE_INSN) {
                        if (oInstruction.getOpcode() == Opcodes.NEW) {
                            oUninitCount++;
                        }
                    }
                    if (nInstruction.getType() == AbstractInsnNode.TYPE_INSN) {
                        if (nInstruction.getOpcode() == Opcodes.NEW) {
                            nUninitCount++;
                        }
                    }
                    if (oInstruction.getType() == AbstractInsnNode.METHOD_INSN) {
                        MethodInsnNode mi = (MethodInsnNode) oInstruction;
                        if (mi.getOpcode() == INVOKESPECIAL && mi.name.equals("<init>")) {
                            if (oUninitCount == 0) {
                                // this is the one!
                                oInvokeSpecialDescriptor = mi.desc;
                            } else {
                                oUninitCount--;
                            }
                        }
                    }
                    if (nInstruction.getType() == AbstractInsnNode.METHOD_INSN) {
                        MethodInsnNode mi = (MethodInsnNode) nInstruction;
                        if (mi.getOpcode() == INVOKESPECIAL && mi.name.equals("<init>")) {
                            if (nUninitCount == 0) {
                                // this is the one!
                                nInvokeSpecialDescriptor = mi.desc;
                            } else {
                                nUninitCount--;
                            }
                        }
                    }
                }
                // Has the invokespecial changed?
                if (oInvokeSpecialDescriptor == null) {
                    if (nInvokeSpecialDescriptor != null) {
                        md.setInvokespecialChanged(oInvokeSpecialDescriptor, nInvokeSpecialDescriptor);
                    }
                } else {
                    if (!oInvokeSpecialDescriptor.equals(nInvokeSpecialDescriptor)) {
                        md.setInvokespecialChanged(oInvokeSpecialDescriptor, nInvokeSpecialDescriptor);
                    }
                }
                if (codeChange) {
                    md.setCodeChanged(oInstructions.toArray(), nInstructions.toArray());
                }
            }
        }
        if (md.hasAnyChanges()) {
            // it needs recording
            td.addChangedMethod(md);
        }

    }

    private static boolean sameInstruction(AbstractInsnNode o, AbstractInsnNode n) {
        if (o.getType() != o.getType() || o.getOpcode() != n.getOpcode()) {
            return false;
        }
        switch (o.getType()) {
        case (AbstractInsnNode.INSN): // 0
            if (!sameInsnNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.INT_INSN): // 1
            if (!sameIntInsnNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.VAR_INSN): // 2
            if (!sameVarInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.TYPE_INSN):// 3
            if (!sameTypeInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.FIELD_INSN): // 4
            if (!sameFieldInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.METHOD_INSN): // 5
            if (!sameMethodInsnNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.JUMP_INSN): // 6
            if (!sameJumpInsnNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.LABEL): // 7
            if (!sameLabelNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.LDC_INSN): // 8
            if (!sameLdcInsnNode(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.IINC_INSN): // 9
            if (!sameIincInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.TABLESWITCH_INSN): // 10
            if (!sameTableSwitchInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.LOOKUPSWITCH_INSN): // 11
            if (!sameLookupSwitchInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.MULTIANEWARRAY_INSN): // 12
            if (!sameMultiANewArrayInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.FRAME): // 13
            if (!sameFrameInsn(o, n)) {
                return false;
            }
            break;
        case (AbstractInsnNode.LINE): // 14
            if (!sameLineNumberNode(o, n)) {
                return false;
            }
            break;
        default:
            throw new IllegalStateException("nyi " + o.getType());
        }
        return true;
    }

    private static boolean sameFrameInsn(AbstractInsnNode o, AbstractInsnNode n) {
        // given that these nodes are computed based on everything else.  if everything else is the same then these
        // must be the same.  A full comparison could be a little ugly as different frames can be equivalent (maybe
        // the compiler produces an incremental frame on one run then a full frame on the next).
        return true;
    }

    private static boolean sameMultiANewArrayInsn(AbstractInsnNode o, AbstractInsnNode n) {
        if (!(n instanceof MultiANewArrayInsnNode)) {
            return false;
        }
        MultiANewArrayInsnNode mnao = (MultiANewArrayInsnNode) o;
        MultiANewArrayInsnNode mnan = (MultiANewArrayInsnNode) n;
        if (!mnao.desc.equals(mnan.desc)) {
            return false;
        }
        if (mnao.dims != mnan.dims) {
            return false;
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private static boolean sameLookupSwitchInsn(AbstractInsnNode o, AbstractInsnNode n) {
        if (!(n instanceof LookupSwitchInsnNode)) {
            return false;
        }
        LookupSwitchInsnNode lsio = (LookupSwitchInsnNode) o;
        LookupSwitchInsnNode lsin = (LookupSwitchInsnNode) n;
        if (sameLabels(lsio.dflt, lsin.dflt)) {
            return false;
        }
        List<Integer> keyso = lsio.keys;
        List<Integer> keysn = lsin.keys;
        if (keyso.size() != keysn.size()) {
            return false;
        }
        for (int i = 0, max = keyso.size(); i < max; i++) {
            if (keyso.get(i) != keysn.get(i)) {
                return false;
            }
        }
        List<LabelNode> labelso = lsio.labels;
        List<LabelNode> labelsn = lsin.labels;
        if (labelso.size() != labelsn.size()) {
            return false;
        }
        for (int i = 0, max = labelso.size(); i < max; i++) {
            if (!sameLabelNode(labelso.get(i), labelsn.get(i))) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private static boolean sameTableSwitchInsn(AbstractInsnNode o, AbstractInsnNode n) {
        if (!(n instanceof TableSwitchInsnNode)) {
            return false;
        }
        TableSwitchInsnNode tsio = (TableSwitchInsnNode) o;
        TableSwitchInsnNode tsin = (TableSwitchInsnNode) n;
        if (sameLabels(tsio.dflt, tsin.dflt)) {
            return false;
        }
        if (tsio.min != tsin.min) {
            return false;
        }
        if (tsio.max != tsin.max) {
            return false;
        }
        List<LabelNode> labelso = tsio.labels;
        List<LabelNode> labelsn = tsin.labels;
        if (labelso.size() != labelsn.size()) {
            return false;
        }
        for (int i = 0, max = labelso.size(); i < max; i++) {
            if (!sameLabelNode(labelso.get(i), labelsn.get(i))) {
                return false;
            }
        }
        return true;
    }

    private static boolean sameLabels(LabelNode lno, LabelNode lnn) {
        // TODO implement?
        return false;
    }

    private static boolean sameFieldInsn(AbstractInsnNode o, AbstractInsnNode n) {
        FieldInsnNode oi = (FieldInsnNode) o;
        if (!(n instanceof FieldInsnNode)) {
            return false;
        }
        FieldInsnNode ni = (FieldInsnNode) n;
        return oi.name.equals(ni.name) && oi.desc.equals(ni.desc) && oi.owner.equals(ni.owner);
    }

    private static boolean sameMethodInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
        MethodInsnNode oi = (MethodInsnNode) o;
        if (!(n instanceof MethodInsnNode)) {
            return false;
        }
        MethodInsnNode ni = (MethodInsnNode) n;
        return oi.name.equals(ni.name) && oi.desc.equals(ni.desc) && oi.owner.equals(ni.owner);
    }

    private static boolean sameVarInsn(AbstractInsnNode o, AbstractInsnNode n) {
        VarInsnNode oi = (VarInsnNode) o;
        if (!(n instanceof VarInsnNode)) {
            return false;
        }
        VarInsnNode ni = (VarInsnNode) n;
        return oi.var == ni.var;
    }

    private static boolean sameInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
        InsnNode oi = (InsnNode) o;
        if (!(n instanceof InsnNode)) {
            return false;
        }
        InsnNode ni = (InsnNode) n;
        return oi.getOpcode() == ni.getOpcode();
    }

    private static boolean sameJumpInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
        //      JumpInsnNode oJumpInsnNode = (JumpInsnNode) o;
        if (!(n instanceof JumpInsnNode)) {
            return false;
        }
        //      JumpInsnNode nJumpInsnNode = (JumpInsnNode) n;
        // TODO tricky to compare destinations when captured as labels with no exposed identifier/position
        return true;
    }

    private static boolean sameLdcInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
        LdcInsnNode oi = (LdcInsnNode) o;
        if (!(n instanceof LdcInsnNode)) {
            return false;
        }
        LdcInsnNode ni = (LdcInsnNode) n;
        Object ocst = oi.cst;
        if (ocst instanceof Integer) {
            if (!(ni.cst instanceof Integer)) {
                return false;
            }
            return ((Integer) ocst).equals(ni.cst);
        }
        if (ocst instanceof Float) {
            if (!(ni.cst instanceof Float)) {
                return false;
            }
            return ((Float) ocst).equals(ni.cst);
        }
        if (ocst instanceof Long) {
            if (!(ni.cst instanceof Long)) {
                return false;
            }
            return ((Long) ocst).equals(ni.cst);
        }
        if (ocst instanceof Double) {
            if (!(ni.cst instanceof Double)) {
                return false;
            }
            return ((Double) ocst).equals(ni.cst);
        }
        if (ocst instanceof String) {
            if (!(ni.cst instanceof String)) {
                return false;
            }
            return ((String) ocst).equals(ni.cst);
        }
        // must be Type
        return ((Type) ocst).equals(ni.cst);
    }

    private static boolean sameIntInsnNode(AbstractInsnNode o, AbstractInsnNode n) {
        IntInsnNode oi = (IntInsnNode) o;
        if (!(n instanceof IntInsnNode)) {
            return false;
        }
        IntInsnNode ni = (IntInsnNode) n;
        return oi.operand == ni.operand;
    }

    private static boolean sameLineNumberNode(AbstractInsnNode o, AbstractInsnNode n) {
        LineNumberNode oi = (LineNumberNode) o;
        if (!(n instanceof LineNumberNode)) {
            return false;
        }
        LineNumberNode ni = (LineNumberNode) n;
        return oi.line == ni.line;
        // TODO check oi.start?
    }

    private static boolean sameIincInsn(AbstractInsnNode o, AbstractInsnNode n) {
        IincInsnNode oi = (IincInsnNode) o;
        if (!(n instanceof IincInsnNode)) {
            return false;
        }
        IincInsnNode ni = (IincInsnNode) n;
        return oi.var == ni.var && oi.incr == ni.incr;
    }

    private static boolean sameTypeInsn(AbstractInsnNode o, AbstractInsnNode n) {
        TypeInsnNode oi = (TypeInsnNode) o;
        if (!(n instanceof TypeInsnNode)) {
            return false;
        }
        TypeInsnNode ni = (TypeInsnNode) n;
        return oi.desc.equals(ni.desc);
    }

    /**
     * Compare two labels to check they are the same.
     * 
     * @param o 'old' label
     * @param n 'new' label
     * @return true if they are different
     */
    private static boolean sameLabelNode(AbstractInsnNode o, AbstractInsnNode n) {
        //      LabelNode oi = (LabelNode) o;
        if (!(n instanceof LabelNode)) {
            return false;
        }
        //      LabelNode ni = (LabelNode) n;

        // TODO tricky to get right.  Unfortunately the positions aren't always available - and we can't check if they are, we have to call the
        // getOffset() method on label and catch an exception if they aren't
        return true;
    }

    private static String compareAnnotations(List<AnnotationNode> oldAnnos, List<AnnotationNode> newAnnos) {
        if (oldAnnos == null) {
            if (newAnnos == null) {
                return "";
            }
            oldAnnos = Collections.emptyList();
        }
        if (newAnnos == null) {
            newAnnos = Collections.emptyList();
        }
        StringBuilder diff = new StringBuilder();
        // Which have been removed
        for (AnnotationNode o : oldAnnos) {
            boolean found = false;
            String oFormatted = Utils.annotationNodeFormat(o);
            for (AnnotationNode n : newAnnos) {
                String nFormatted = Utils.annotationNodeFormat(n);
                if (oFormatted.equals(nFormatted)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                diff.append("-").append(oFormatted);
            }
        }
        // Which have been added
        for (AnnotationNode n : newAnnos) {
            boolean found = false;
            String nFormatted = Utils.annotationNodeFormat(n);
            for (AnnotationNode o : oldAnnos) {
                String oFormatted = Utils.annotationNodeFormat(o);
                if (oFormatted.equals(nFormatted)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                diff.append("+").append(nFormatted);
            }
        }
        return diff.toString();
    }

    @SuppressWarnings("unchecked")
    private static void computeTypeDelta(ClassNode oldClassNode, ClassNode newClassNode, TypeDelta td) {
        //      if (oldClassNode.version != newClassNode.version) {
        //         td.setTypeVersionChange(oldClassNode.version, newClassNode.version);
        //      }
        if (oldClassNode.access != newClassNode.access) {
            // Is it only because of 0x20000 - that appears to represent Deprecated!
            if ((oldClassNode.access & 0xffff) != (newClassNode.access & 0xffff)) {
                td.setTypeAccessChange(oldClassNode.access, newClassNode.access);
            }
        }
        if (!oldClassNode.name.equals(newClassNode.name)) {
            td.setTypeNameChange(oldClassNode.name, newClassNode.name);
        }
        //      if (oldClassNode.signature == null) {
        //         if (newClassNode.signature != null) {
        //            td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
        //         }
        //      } else if (newClassNode.signature == null) {
        //         if (oldClassNode.signature != null) {
        //            td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
        //         }
        //      } else if (!oldClassNode.signature.equals(newClassNode.signature)) {
        //         td.setTypeSignatureChange(oldClassNode.signature, newClassNode.signature);
        //      }
        if (oldClassNode.superName == null) {
            if (newClassNode.superName != null) {
                td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
            }
        } else if (newClassNode.superName == null) {
            if (oldClassNode.superName != null) {
                td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
            }
        } else if (!oldClassNode.superName.equals(newClassNode.superName)) {
            td.setTypeSuperNameChange(oldClassNode.superName, newClassNode.superName);
        }
        if (oldClassNode.interfaces.size() == 0) {
            if (newClassNode.interfaces.size() != 0) {
                td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
            }
        } else if (newClassNode.interfaces.size() == 0) {
            if (oldClassNode.interfaces.size() != 0) {
                td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
            }
        } else {
            if (oldClassNode.interfaces.size() != newClassNode.interfaces.size()) {
                td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
            }
            HashSet<String> oldInterfaceSet = new HashSet<String>(oldClassNode.interfaces);
            HashSet<String> newInterfaceSet = new HashSet<String>(newClassNode.interfaces);
            if (!oldInterfaceSet.equals(newInterfaceSet)) { // TODO expensive? keep the interfaces list sorted instead?
                td.setTypeInterfacesChange(oldClassNode.interfaces, newClassNode.interfaces);
            }
        }
    }
}