JVM Advent

The JVM Programming Advent Calendar

JavaParser to generate, analyze and modify Java code

As developers we frequently look in contempt to people doing repetitive work manually.

They should automate that, we think.

Still, we do all activities related to coding by hand. Sure, we use fancy IDEs that can perform some little refactoring for us but that is basically the end of it. We do not taste our own medicine.

Let’s change that. Let’s see how we can write code to:

  • Generate the boring, repetitive Java code we have to write
  • Analyze our code to answer some questions about it
  • Do some code processing & refactoring

The good thing is that we are going to achieve all of this with one set of libraries: JavaParser and its little brother called JavaSymbolSolver.

getting started

Well, that is an easy one: just add JavaSymbolSolver to your dependencies.

What is JavaSymbolSolver? It is a library that complements JavaParser giving to it some pretty powerful features which are necessary to answer more complex questions about code.

JavaSymbolSolver depends on JavaParser so you just need to add JavaSymbolSolver and Maven or Gradle will get also JavaParser for you.

I assume you know how to use Maven or Gradle. If you don’t, stop reading this and go learn that first!

Generating code with javaparser

There are several situations in which you may want to generate Java code. For example, you could want to generate code based on some external data, like a database schema or a REST API.

You may also want to translate some other language into Java. For example, I design DSLs for a living and while the users get to see only the DSLs I build for them I frequently generate Java behind the scenes and compile that.

Sometimes you want just to generate boilerplate code, like I used to dp when working with JavaEE and all those layers (who can remember how boring was to write EJB?).

Whatever is your reason for generating code you can use JavaParser. JavaParser does not ask question, it is just there to help you.

Let’s see how we can generate a class with two fields, a constructor and two getters. Nothing terribly advanced but it should give you a feeling of what it means to use JavaParser for code generation.

CompilationUnit cu = new CompilationUnit();

cu.setPackageDeclaration("jpexample.model");

ClassOrInterfaceDeclaration book = cu.addClass("Book");
book.addField("String", "title");
book.addField("Person", "author");

book.addConstructor(Modifier.PUBLIC)
        .addParameter("String", "title")
        .addParameter("Person", "author")
        .setBody(new BlockStmt()
                .addStatement(new ExpressionStmt(new AssignExpr(
                        new FieldAccessExpr(new ThisExpr(), "title"),
                        new NameExpr("title"),
                        AssignExpr.Operator.ASSIGN)))
                .addStatement(new ExpressionStmt(new AssignExpr(
                        new FieldAccessExpr(new ThisExpr(), "author"),
                        new NameExpr("author"),
                        AssignExpr.Operator.ASSIGN))));

book.addMethod("getTitle", Modifier.PUBLIC).setBody(
        new BlockStmt().addStatement(new ReturnStmt(new NameExpr("title"))));

book.addMethod("getAuthor", Modifier.PUBLIC).setBody(
        new BlockStmt().addStatement(new ReturnStmt(new NameExpr("author"))));

System.out.println(cu.toString());

That last instruction print your code, fresh and ready to be compiled. You may want to save the code into a file instead of printing it but you get the idea.

analyzing code with javaparser

There are many different questions you could ask about your code, many different ways to analyze it.

First of all let’s parse all source files of our project:

// Parse all source files
SourceRoot sourceRoot = new SourceRoot(myProjectSourceDir.toPath());
sourceRoot.setParserConfiguration(parserConfiguration);
List<ParseResult> parseResults = sourceRoot.tryToParse("");

// Now get all compilation unitsList 
allCus = parseResults.stream()        
        .filter(ParseResult::isSuccessful)        
        .map(r -> r.getResult().get())        
        .collect(Collectors.toList());

Let’s also create a method to get all nodes of a certain type among all our compilation units:

public static  List getNodes(List cus, Class nodeClass) {
    List res = new LinkedList();
    cus.forEach(cu -> res.addAll(cu.findAll(nodeClass)));
    return res;
}

Then let’s start asking questions, like:

How many methods take more than 3 parameters?

long n = getNodes(allCus, MethodDeclaration.class)        .stream()        .filter(m -> m.getParameters().size() > 3)
    .count();System.out.println("N of methods with 3+ params: " + n);

What are the three top classes with most methods?

getNodes(allCus, ClassOrInterfaceDeclaration.class)        .stream()        .filter(c -> !c.isInterface())        .sorted(Comparator.comparingInt(o -> 
        -1 * o.getMethods().size()))        .limit(3)        .forEach(c -> 
        System.out.println(c.getNameAsString() + ": " +             c.getMethods().size() + " methods"));

Ok, you get the idea. Now go examine your code. You do not have anything to hide, right?

transforming code with javaparser

Suppose you are the happy user of a certain library. You have added it to your dependencies years ago and used it happily ever after. Time has passed and you have used it more and more, basically all over your project.

One day a new version of that useful library comes up and you decide you want to update your dependencies. Now, in the new library they have removed one of the methods you were using. Sure it was deprecated and it was named oldMethod (which could have told you something…).

Now oldMethod has been replaced by newMethod. The newMethod takes 3 parameters: the first two are the same as oldMethod, they are just inverted the third one is a boolean, which should be set to true to get the same behavior we were getting with oldMethod.

You have hundreds of calls to oldMethod… are you going to change them one by one? Well, maybe, if you are charging by the hour. Or you could just use JavaParser instead.

First let’s find all the calls to the old method in a certain file, a.k.a. CompilationUnit in JavaParser parlanse:

myCompilationUnit.findAll(ethodCallExpr.class)
        .stream()
        .filter(m -> m.resolveInvokedMethod()                
             .getQualifiedSignature()                
             .equals("foo.MyClass.oldMethod(java.lang.String, int)"))        
        .forEach(m -> m.replace(replaceCallsToOldMethod(m)));

And then let’s transform the old calls in the new ones:

public MethodCallExpr replaceCallsToOldMethod(MethodCallExpr methodCall) {    
     MethodCallExpr newMethodCall = new MethodCallExpr(
             methodCall.getScope().get(), "newMethod");    
     newMethodCall.addArgument(methodCall.getArgument(1));    
     newMethodCall.addArgument(methodCall.getArgument(0));    
     newMethodCall.addArgument(new BooleanLiteralExpr(true));    
     return newMethodCall;
}

Cool, now we just need to get the code for our modified CompilationUnit and just save it into the Java file.

Long life to newMethod!

where to find out more about javaparser

There are tons of features of JavaParser we have not seen:

  • JavaParser can handle comments, figuring out to which elements they refer to
  • JavaParser can do lexical preservation or pretty printing: your choice
  • It can find out to which method declaration a method call refers to, which ancestors a certain class has, and much more thanks to the integration with JavaSymbolSolver
  • It can export the AST to JSON, XML, YAML, and even generate diagrams using Graphviz!

Where can you learn about all this stuff?

Here there are a few resources:

  • We wrote a book on JavaParser & JavaSymbolSolver, available for free. It is named JavaParser: Visited
  • The blog of the great Matozoid: he is the glorious maintainer of JavaParser, the unstoppable force that push a new release out every-single-week. Who knows better about JavaParser?
  • My humble blog on Language Engineering. I am the maintainer of JavaSymbolSolver and I try to help as the second in command at JavaParser. A distant second 🙂
  • The website of the project: not very rich in content at the moment but we are working on it
  • The gitter channel: do you have questions? Asked them there

summary

It is hardly the case that you can learn how to use one tool to do three different things. By learning how to use JavaParser you can analyze, generate, and modify Java code.

Well, it feels like Christmas, doesn’t it?

Author: ftomassetti

I founded Strumenta, a Consulting Studio on Language Engineering. We build languages, DSLs, editors, parsers, compilers, interpreters and that sort of stuff. Before that I got a PhD, I lived in Italy, Germany, Ireland, and France, worked for TripAdvisor and Groupon, created to many projects on GitHub and contributed to JavaParser and JavaSymbolSolver.

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén