Custom refactoring with Eclipse JDT library

Eclipse source code clean-up wizard and other refactoring tools are great, but sometimes they are just not quite powerful enough. For example a large legacy application with 100s of instances of non-standard Java package and class names. These could be cleaned by refactoring each in turn. Additionally Eclipse “update fully qualified names in non-Java text files” only applies to files in the current project, not the wider workspace.

So what can we do… Regex? Hmm. Well no, works for 99% of cases but you could easily introduce errors you’ll not spot until weeks later, especially on large projects.

Eclipse JDT to the rescue. Eclipse JDT is one of the underlying libraries that does the heavy lifting of parsing Java into an AST which loads of other tools build upon. Other Java AST libraries are available, see Java Parser etc.

We could use Eclipse JDT as part of a plugin, or standalone. The following is a simple example of how to drive JDT to rewrite a file

Firstly the dependency

implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.28.0'

Some example input

import java.util.ArrayList;
import java.util.List;

public class Input {
    private List rawType;
    private List<String> genericType;
    private java.util.Vector vector;

    public static void main(String[] args) {
        System.out.println("foo " + List.class.toString());
        List rawLocal = new ArrayList<>();
        List<String> genericLocal = new ArrayList<>();
    }
}

Now we parse the file

String source = Files.readString(Path.of("src/main/java/com/adamish/eclipsejdtexperiment/Input.java"));
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(source.toCharArray());
parser.setResolveBindings(true);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
cu.recordModifications();

Then we apply some modifications using the visitor pattern. In our example we substitute some classes.

Note the JDT library makes you really work for it with respect to changing type definitions

Map<String, String> substitutes = new HashMap<>();
substitutes.put("java.util.List", "org.example.ListFoo");
substitutes.put("java.util.Vector", "org.example.VectorFoo");

cu.accept(new ASTVisitor() {

    /**
     * will handle imports and FQNs
     */
    @Override
    public boolean visit(QualifiedName node) {
        String fqn = node.getFullyQualifiedName();
        String updated = substitutes.get(fqn);
        if (updated != null) {
            Name name = node.getAST().newName(getPackageName(updated));
            node.getName().setIdentifier(getClassName(updated));
            node.setQualifier(name);
        }
        return super.visit(node);
    }

    /**
     * unqualified references to types
     */
    @Override
    public boolean visit(SimpleType node) {
        for (Entry<String, String> entry : substitutes.entrySet()) {
            String clazzName = getClassName(entry.getKey());
            if (node.getName().getFullyQualifiedName().equals(clazzName)) {
                node.setName(node.getAST().newName(getClassName(entry.getValue())));
            }
        }
        return super.visit(node);
    }
});

Now we write the file back to disk, well here we just log it

Document document = new Document(source);
TextEdit edits = cu.rewrite(document, null);
edits.apply(document);

String updated = document.get();

System.out.println(updated);

Now the output, see the import statements and all references are magically updated. The other great advantage is that JDT preserves whitespace formatting.

import java.util.ArrayList;
import org.example.ListFoo;

public class Input {
    private ListFoo rawType;
    private ListFoo<String> genericType;
    private org.example.VectorFoo vector;

    public static void main(String[] args) {
        System.out.println("foo " + ListFoo.class.toString());
        ListFoo rawLocal = new ArrayList<>();
        ListFoo<String> genericLocal = new ArrayList<>();
    }
}