1

I'd like to find the methods which changed between two arbitrary Java files.

What I've Tried

I've tried using diff (GNU diffutils 3.3) to find changes to lines in the file and diff --show-c-function connect those lines to the changed method. Unfortunately, in Java this lists the class, not the function.

I also tried git diff which seems to properly be able to find the changed function (at least as displayed on GitHub), but it doesn't always list the full signature and my files are not in the same Git repository (https://github.com/apache/commons-math/commit/34adc606601cb578486d4a019b4655c5aff607b5).

Desired Results

Input:

~/repos/d4jBugs/Math_54_buggy/src/main/java/org/apache/commons/math/dfp/Dfp.java
~/repos/d4jBugs/Math_54_fixed/src/main/java/org/apache/commons/math/dfp/Dfp.java

State of Files:

The changed methods between those two files are public double toDouble() and protected Dfp(final DfpField field, double x)

Output: (fully qualified names)

org.apache.commons.math.dfp.Dfp.toDouble()
org.apache.commons.math.dfp.Dfp(final DfpField field, double x)

Summary

Can I find the modified methods with the GNU diffutils tool or git diff and if yes, how would I do that? (Note: I'm not bound to those tools and am happy to install something else if needed.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Loren
  • 9,783
  • 4
  • 39
  • 49
  • If you don't need a diff, why bother with a diff? Just check if the bytecode of the methods (they're methods, not functions in Java) is different. – pvg Oct 17 '17 at 17:32
  • I've edited my question to hopefully be clear. Though, I didn't get a direct comment from @Bohemian so I'm not sure his exact complaint. – Loren Oct 17 '17 at 21:16
  • @pvg Can you explain how to go about checking the bytecode and getting the signature of the method if the bytecode has changed? It sounds like a good idea. I'm just not sure where to start to do it. – Loren Oct 17 '17 at 21:17
  • Good tools is KDIFF3 and PeMerge – Marcos Vasconcelos Oct 17 '17 at 21:58
  • Duplicate of https://stackoverflow.com/questions/5721447/using-git-to-identify-all-modified-functions-in-a-revision? – Martin Monperrus Apr 15 '18 at 11:55

1 Answers1

0

I used JavaParser 3.4.4, but it probably works but has not been tested with other versions.

It can be imported in Gradle with:

compile group: 'com.github.javaparser', name: 'javaparser-core', version: '3.4.4'

You can use my class like:

HashSet<String> changedMethods = MethodDiff.methodDiffInClass(
        oldFileNameWithPath,
        newFileNameWithPath
    );

MethodDiff Source:

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.CallableDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.printer.PrettyPrinterConfiguration;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * Created by Loren Klingman on 10/19/17.
 * Finds Changes Between Methods of Two Java Source Files
 */
public class MethodDiff {

    private static PrettyPrinterConfiguration ppc = null;

    class ClassPair {
        final ClassOrInterfaceDeclaration clazz;
        final String name;
        ClassPair(ClassOrInterfaceDeclaration c, String n) {
            clazz = c;
            name = n;
        }
    }

    public static PrettyPrinterConfiguration getPPC() {
        if (ppc != null) {
            return ppc;
        }
        PrettyPrinterConfiguration localPpc = new PrettyPrinterConfiguration();
        localPpc.setColumnAlignFirstMethodChain(false);
        localPpc.setColumnAlignParameters(false);
        localPpc.setEndOfLineCharacter("");
        localPpc.setIndent("");
        localPpc.setPrintComments(false);
        localPpc.setPrintJavadoc(false);

        ppc = localPpc;
        return ppc;
    }

    public static <N extends Node> List<N> getChildNodesNotInClass(Node n, Class<N> clazz) {
        List<N> nodes = new ArrayList<>();
        for (Node child : n.getChildNodes()) {
            if (child instanceof ClassOrInterfaceDeclaration) {
                // Don't go into a nested class
                continue;
            }
            if (clazz.isInstance(child)) {
                nodes.add(clazz.cast(child));
            }
            nodes.addAll(getChildNodesNotInClass(child, clazz));
        }
        return nodes;
    }

    private List<ClassPair> getClasses(Node n, String parents, boolean inMethod) {
        List<ClassPair> pairList = new ArrayList<>();
        for (Node child : n.getChildNodes()) {
            if (child instanceof ClassOrInterfaceDeclaration) {
                ClassOrInterfaceDeclaration c = (ClassOrInterfaceDeclaration)child;
                String cName = parents+c.getNameAsString();
                if (inMethod) {
                    System.out.println(
                            "WARNING: Class "+cName+" is located inside a method. We cannot predict its name at"
                            + " compile time so it will not be diffed."
                    );
                } else {
                    pairList.add(new ClassPair(c, cName));
                    pairList.addAll(getClasses(c, cName + "$", inMethod));
                }
            } else if (child instanceof MethodDeclaration || child instanceof ConstructorDeclaration) {
                pairList.addAll(getClasses(child, parents, true));
            } else {
                pairList.addAll(getClasses(child, parents, inMethod));
            }
        }
        return pairList;
    }

    private List<ClassPair> getClasses(String file) {
        try {
            CompilationUnit cu = JavaParser.parse(new File(file));
            return getClasses(cu, "", false);
        } catch (FileNotFoundException f) {
            throw new RuntimeException("EXCEPTION: Could not find file: "+file);
        }
    }

    public static String getSignature(String className, CallableDeclaration m) {
        return className+"."+m.getSignature().asString();
    }

    public static HashSet<String> methodDiffInClass(String file1, String file2) {
        HashSet<String> changedMethods = new HashSet<>();
        HashMap<String, String> methods = new HashMap<>();

        MethodDiff md = new MethodDiff();

        // Load all the method and constructor values into a Hashmap from File1
        List<ClassPair> cList = md.getClasses(file1);

        for (ClassPair c : cList) {
            List<ConstructorDeclaration> conList = getChildNodesNotInClass(c.clazz, ConstructorDeclaration.class);
            List<MethodDeclaration> mList = getChildNodesNotInClass(c.clazz, MethodDeclaration.class);
            for (MethodDeclaration m : mList) {
                String methodSignature = getSignature(c.name, m);

                if (m.getBody().isPresent()) {
                    methods.put(methodSignature, m.getBody().get().toString(getPPC()));
                } else {
                    System.out.println("Warning: No Body for "+file1+" "+methodSignature);
                }
            }
            for (ConstructorDeclaration con : conList) {
                String methodSignature = getSignature(c.name, con);
                methods.put(methodSignature, con.getBody().toString(getPPC()));
            }
        }

        // Compare everything in file2 to what is in file1 and log any differences
        cList = md.getClasses(file2);
        for (ClassPair c : cList) {
            List<ConstructorDeclaration> conList = getChildNodesNotInClass(c.clazz, ConstructorDeclaration.class);
            List<MethodDeclaration> mList = getChildNodesNotInClass(c.clazz, MethodDeclaration.class);
            for (MethodDeclaration m : mList) {
                String methodSignature = getSignature(c.name, m);

                if (m.getBody().isPresent()) {
                    String body1 = methods.remove(methodSignature);
                    String body2 = m.getBody().get().toString(getPPC());
                    if (body1 == null || !body1.equals(body2)) {
                        // Javassist doesn't add spaces for methods with 2+ parameters...
                        changedMethods.add(methodSignature.replace(" ", ""));
                    }
                } else {
                    System.out.println("Warning: No Body for "+file2+" "+methodSignature);
                }
            }
            for (ConstructorDeclaration con : conList) {
                String methodSignature = getSignature(c.name, con);
                String body1 = methods.remove(methodSignature);
                String body2 = con.getBody().toString(getPPC());
                if (body1 == null || !body1.equals(body2)) {
                    // Javassist doesn't add spaces for methods with 2+ parameters...
                    changedMethods.add(methodSignature.replace(" ", ""));
                }
            }
            // Anything left in methods was only in the first set and so is "changed"
            for (String method : methods.keySet()) {
                // Javassist doesn't add spaces for methods with 2+ parameters...
                changedMethods.add(method.replace(" ", ""));
            }
        }
        return changedMethods;
    }

    private static void removeComments(Node node) {
        for (Comment child : node.getAllContainedComments()) {
            child.remove();
        }
    }
}
Loren
  • 9,783
  • 4
  • 39
  • 49