Search code examples
javaassemblystatic-analysisjava-bytecode-asm

Can ASM method-visitors be used with interfaces?


I need to write a tool that lists the classes that call methods of specified interfaces. It will be used as part of the build process of a large java application consisting of many modules. The goal is to automatically document the dependencies between certain java modules.

I found several tools for dependency analysis, but they don't work on the method level, just for packages or jars. Finally I found ASM, that seems to do what I need.

The following code prints the method dependencies of all class files in a given directory:

import java.io.*;
import java.util.*;

import org.objectweb.asm.ClassReader;

public class Test {

    public static void main(String[] args) throws Exception {

        File dir = new File(args[0]);

        List<File> classFiles = new LinkedList<File>();
        findClassFiles(classFiles, dir);

        for (File classFile : classFiles) {
            InputStream input = new FileInputStream(classFile);
            new ClassReader(input).accept(new MyClassVisitor(), 0);
            input.close();
        }
    }

    private static void findClassFiles(List<File> list, File dir) {
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                findClassFiles(list, file);
            } else if (file.getName().endsWith(".class")) {
                list.add(file);
            }
        }
    }
}

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.EmptyVisitor;

public class MyClassVisitor extends EmptyVisitor {

    private String className;

    @Override
    public void visit(int version, int access, String name, String signature,
            String superName, String[] interfaces) {
        this.className = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {

        System.out.println(className + "." + name);
        return new MyMethodVisitor();
    }
}

import org.objectweb.asm.commons.EmptyVisitor;

public class MyMethodVisitor extends EmptyVisitor {

    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
            String desc) {

        String key = owner + "." + name;
        System.out.println("  " + key);
    }
}

The Problem:

The code works for regular classes only! If the class file contains an interface, visitMethod is called, but not visitMethodInsn. I don't get any info about the callers of interface methods.

Any ideas?


Solution

  • I have to admit, I was confused...

    I thought that asm visitors do some magic to give me the list of all callers of a given method, like a stacktrace. Instead they justs parse classes and method bodies. Fortunatly, this is totally sufficent for my needs as I can build the call tree by myself.

    The following code lists all methods that are called by other methods, checking class files in given directory (and subdirectories) only:


    import java.io.*;
    import java.util.*;
    
    import org.objectweb.asm.ClassReader;
    
    public class Test {
    
        public static void main(String[] args) throws Exception {
    
            File dir = new File(args[0]);
    
            Map<String, Set<String>> callMap = new HashMap<String, Set<String>>();
    
            List<File> classFiles = new LinkedList<File>();
            findClassFiles(classFiles, dir);
    
            for (File classFile : classFiles) {
                InputStream input = new FileInputStream(classFile);
                new ClassReader(input).accept(new MyClassVisitor(callMap), 0);
                input.close();
            }
    
            for (Map.Entry<String, Set<String>> entry : callMap.entrySet()) {
                String method = entry.getKey();
                Set<String> callers = entry.getValue();
    
                if (callers != null && !callers.isEmpty()) {
                    System.out.println(method);
                    for (String caller : callers) {
                        System.out.println("    " + caller);
                    }
                }
            }
        }
    
        private static void findClassFiles(List<File> list, File dir) {
            for (File file : dir.listFiles()) {
                if (file.isDirectory()) {
                    findClassFiles(list, file);
                } else if (file.getName().endsWith(".class")) {
                    list.add(file);
                }
            }
        }
    }
    

    import java.util.*;
    
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.commons.EmptyVisitor;
    
    public class MyClassVisitor extends EmptyVisitor {
    
        private String className;
        private Map<String, Set<String>> callMap;
    
        public MyClassVisitor(Map<String, Set<String>> callMap) {
            this.callMap = callMap;
        }
    
        @Override
        public void visit(int version, int access, String name, String signature,
                String superName, String[] interfaces) {
            this.className = name;
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                String signature, String[] exceptions) {
    
            return new MyMethodVisitor(className + "." + name, callMap);
        }
    }
    

    import java.util.*;
    
    import org.objectweb.asm.commons.EmptyVisitor;
    
    public class MyMethodVisitor extends EmptyVisitor {
    
        private String currentMethod;
        private Map<String, Set<String>> callMap;
    
        public MyMethodVisitor(String currentMethod,
                Map<String, Set<String>> callMap) {
            this.currentMethod = currentMethod;
            this.callMap = callMap;
        }
    
        @Override
        public void visitMethodInsn(int opcode, String owner, String name,
                String desc) {
    
            String calledMethod = owner + "." + name;
    
            Set<String> callers = callMap.get(calledMethod);
            if (callers == null) {
                callers = new TreeSet<String>();
                callMap.put(calledMethod, callers);
            }
    
            callers.add(currentMethod);
        }
    }