Search code examples
javaeclipseabstract-syntax-treeeclipse-jdt

Preserving type, field and method comment when rewriting a CompilationUnit


TL;DR: How to preserve the Javadoc, line- and block-comment when creating a new java type based on a given type?

Long:

I am batch converting an infinite amount of types (Java classes) based on an unwanted base class towards Java enum types by using a headless eclipse application using JDT. My approach is to create a new EnumDeclaration based on the type information and adding the EnumConstantDeclarations based on the FieldDeclarations (exluding the serialVersionUID) of the initial class type. After that I add the MethodDeclarations (excluding the constructor) by simply adding a clone of the original MethodDeclarations to the BodyDelcaration of the newly created EnumDeclaration. Once I have done that, which is quite straight forward thanks to the great API, I do the following...

// create the EnumDeclaration from the given UnwantedClass CompilationUnit
final EnumDeclaration enumTypeDeclaration = createEnumDeclaration(cu, astRoot, methodDeclarations, ast);
// Find the original UnwantedClass TypeDeclaration and replace it with the new EnumDeclaration
astRoot.accept(new ASTVisitor() {

  @Override
  public boolean visit(final TypeDeclaration node) {
    rewriter.replace(node, enumTypeDeclaration, null);
    return false;
  }
});

...to replace the original Java class Type with the new EnumDeclaration. This works almost perfectly. The only thing missing are all the Line-, Block- and JavadocComment elements of the original Java type. I found out that you can at least retrieve all Comment instances by:

List<Comment> comments = cu.getCommentList();
if (comments != null) {
  for (Comment comment : comments) {
    comment.accept(visitor);
  }
}

That gives me all the comment, but I haven't figured out how to map a Comment instance to a BodyDeclaration, because these Comment instances are basically free floating all over the Source file and are only linked by their startPosition within the Source file.

There is the getAlternateRoot method, but I haven't managed either to utilize that one.

The question is: How do I preserve the Comment instances from the original type and put them at the correct position in the new type?


Solution

  • It seems, there is no straight forward approach to solve this, because javadoc (or just comments in general) in java files have no direct relation to the actual code they are meant to comment. Its just common sense among developers that we are usually put the comment above the code we are commenting. So I took the following path to solve this:

    1. Create an starting position based index which maps an Integer (the starting position) to so called CommentEntry instances, which is basically a tuple consisting of the org.eclipse.jdt.core.dom.Comment and the actual text, which I extract by simply substring the source string of the compilation unit.
    2. I visit my original CompilationUnit and retrieve all relevant ASTNode, which are Type-, Field- and MethodDeclaration instances in my case and map those on their starting positions.
    3. Now I iterate over all mapped CommentEntry positions from 1. and associate them with an ASTNode instance, which is mapped to the same position (from the map of 2.). The result is then a map with ASTNode -> CommentEntry.
    4. While building my new EnumDeclaration I query the map from 3. either by using the actual ASTNode type (TypeDeclaration only exists once), the actual instance of the ASTNode (I use attributes from the orignal FieldDeclarations to create the EnumConstants), or use the ASTMatcher to identify the correct MethodDeclaration, because I had to clone these in order to be able to "copy" them into the new EnumDeclaration Body section.

    The adding of the comment isn't as straight forward as I thought, because in newer JSL you cant simply set the comment String in javadoc (only supported in JSL2, but in this version we hadn't enums). So, I used the following code:

    private void setJavadoc(final BodyDeclaration bodyDeclaration, final CommentEntry commentEntry) {
        final Javadoc javadoc = (Javadoc) ASTNode.copySubtree(ast, commentEntry.getComment());
        final TagElement tagElement = ast.newTagElement();
        final TextElement textElement = ast.newTextElement();
        textElement.setText(commentEntry.getText());
        tagElement.fragments().add(textElement);
        javadoc.tags().add(tagElement);
        bodyDeclaration.setJavadoc(javadoc);
    }
    

    As you can see, the actual comment text can be simply added by using a TextElement, even though it contains Tags. To make it perfect you may need to substring the comment text previously extracted from the source file, because it will contain the /** and */.