Java Specialists' Java Training Europehome of the java specialists' newsletter

The Java Specialists' Newsletter
Issue 0492002-06-05 Category: Software Engineering Java version:

GitHub Subscribe Free RSS Feed

Doclet for finding missing comments

by Dr. Heinz M. Kabutz

Welcome to the 49th edition of The Java(tm) Specialists' Newsletter, sent to over 3700 Java experts in over 82 countries. This week I am going to have a fun time running my Design Patterns Course at Mark Shuttleworth's old company. In the unlikely case that you are unfamiliar with the name "Shuttleworth", it belongs to the first South African in space, the second space tourist, Mark Shuttleworth. His space trip has inspired many young people of South Africa to strive in Science and Mathematics.

NEW: Please see our new "Extreme Java" course, combining concurrency, a little bit of performance and Java 8. Extreme Java - Concurrency & Performance for Java 8.

Doclet for finding missing comments

A few newsletter ago, I made some comments about the fact that I rarely read comments. The response from people was overwhelming. There were very few neutral voices about what I had said: I was called "childish", "inexperienced" from the one camp, and "wise", "at long last someone has the guts to say it" from the other camp. A small detail that readers from both camps missed, was that I never said that I don't write comments, I merely said that I don't read them ;-)

Why do I write comments?

  1. The person who has to maintain my code may not share my enthusiasm for reading code in order to understand what I was doing.
  2. I quite like explaining in my comments the why of what I was doing.
  3. The JavaDocs are a great tool for producing API documentation.

My biggest frustration with JavaDocs is that it is so difficult to remember keeping all the comments up to date all the time. One of my readers in India shared the same frustration, so she wrote a comment checker Doclet. I used her Doclet whenever I was programming, but it wasn't really very object-oriented.

I spent some time last weekend refactoring the program so that the code would be more understandable. This is quite a long newsletter, because of all the code. I have not added comments to the "CommentChecker", you'll have to figure out yourself how it works :-)

We start with the main class called CommentChecker, called by the javadoc system. In this class, I find all the classes from the RootDoc and I run a ClassChecker against them.

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.RootDoc;

public class CommentChecker {
  public static boolean start(RootDoc root) {
    ClassDoc[] classes = root.classes();
    for (int i = 0; i < classes.length; i++) {
      new ClassChecker(classes[i]).check();
    }
    return true;
  }
}

Let's also have two test cases, a class with comments called GoodTest ...

/** This is a test class */
public class GoodTest {
  /**
   * Constructor used for something
   * @param i used for something 
   */
  public GoodTest(int i) {}
  /**
   * No-args constructor for GoodTest.
   */
  public GoodTest() {}
  /** This is a good comment */
  private boolean good;
}

... and a class with invalid or missing comments called BadTest.

public class BadTest {
  public BadTest(int i) {}
  /**
   * @param someone means nothing
   * @return always true
   * @throws bla if something bad happens
   */
  public BadTest() {}
  /**
   * @return nothing at all!
   * @return nothing at all!
   * @throws Exception if nothing happens
   * @throws Exception if something happens
   */
  public void method1() throws NullPointerException, Exception {}
  private boolean bad;
}

In order to call this, we execute the following command. To also check private data members / functions, we add the -private option.

javadoc -private -doclet CommentChecker *Test.java

For the GoodTest class, there is no output to System.err (because no comments are missing!). Depending on your company standards, you can change the Doclet to, for example, insist on an @author tag in the JavaDocs. My comments in the GoodTest class are nonsense of course - they have no meaning! "In the real world", I would have more sensibly named classes than GoodTest and the comments would also add value to the class. The output from running this doclet is:

BadTest misses comment
BadTest.BadTest(int) misses comment
BadTest.BadTest(int) misses comment for parameter "i"
BadTest.BadTest() misses comment
BadTest.BadTest() has unnecessary return comment
BadTest.BadTest() parameter "someone" does not exist
BadTest.BadTest() has unnecessary comment for exception "bla"
BadTest.method1() misses comment
BadTest.method1() has unnecessary return comment
BadTest.method1() has multiple comments for exception "Exception"
BadTest.method1() is missing comments for exception "NullPointerException"
BadTest.bad misses comment

Oh, I haven't shown you the rest of the classes, of course! I just wanted to whet your appetite so that you'll read the rest of this newsletter. As you can see, the output from the Doclet can be really useful if you want to make sure that you (or your client) have added all the necessary comments.

The hierarchy for my checking classes is as follows:

Checker
  |
  +-ClassChecker
  |
  +-ExecutableChecker
  |   |
  |   +-ConstructorChecker
  |   |
  |   +-MethodChecker
  |
  +-FieldChecker

Let's have a look at the Checker superclass:

import com.sun.javadoc.Doc;

/**
 * Abstract superclass for checking a code component.
 */
public abstract class Checker {
  private final Doc doc;
  public Checker(Doc doc) {
    this.doc = doc;
  }
  public abstract void check();
  protected abstract String getDescriptor();
  protected final boolean isEmpty(String s) {
    return s == null || s.trim().length() == 0;
  }  
  public void checkComments() {
    if (isEmpty(doc.commentText()))
      error("misses comment");
  }
  protected void error(String msg) {
    System.err.println(getDescriptor() + ' ' + msg);
  }
}

We keep a handle to "Doc", which we use to test whether this code element has any comments. We also have an abstract check() method, which will be implemented differently for each code element. Each code element has a descriptor that we use to display which element an error belongs to.

Next we look at the class that checks whether a class has adequate comments:

import com.sun.javadoc.*;

/**
 * Check whether the class has comments
 */
public class ClassChecker extends Checker {
  private final ClassDoc doc;
  public ClassChecker(ClassDoc doc) {
    super(doc);
    this.doc = doc;
  }
  protected String getDescriptor() {
    return doc.qualifiedName();
  }
  public void check() {
    checkComments(); // calls superclass
    checkConstructors();
    checkMethods();
    checkFields();
  }
  private void checkConstructors() {
    ConstructorDoc[] constructors = doc.constructors();
    for (int i = 0; i < constructors.length; i++) {
      new ConstructorChecker(this, constructors[i]).check();
    }
  }
  private void checkMethods() {
    MethodDoc[] methods = doc.methods();
    for (int i = 0; i < methods.length; i++) {
      new MethodChecker(this, methods[i]).check();
    }
  }
  private void checkFields() {
    FieldDoc[] fields = doc.fields();
     for (int i = 0; i < fields.length; i++) {
      new FieldChecker(this, fields[i]).check();
    }
  }
}

This leads us to have a look at the ExecutableChecker class, a superclass of checking the comments of methods and constructors. The only difference between methods and constructors (as far as we are concerned) is that the constructor may not have a @return tag.

BTW, on a slightly different note, did you know that the following code compiles? i.e. you can have a method with the same name as the class. It can happen quite easily that you mean to write a constructor, but being a diligent C++ programmer you add the void keyword before the "constructor", thus actually writing a method. I discovered this a few years ago when one of my Bruce Eckel "Handson Java" students did this accidentally.

public class A {
  public void A() {}
}

Back to the problem on hand, a checker for methods and constructors. Since the only difference in our checking has to do with the return value, we make an abstract method called checkReturnComments(). I'll let you figure out the checkParametersForComments() and checkExceptionComments() methods yourself.

import com.sun.javadoc.*;
import java.util.*;

public abstract class ExecutableChecker extends Checker {
  protected final String descriptor;
  private final ExecutableMemberDoc doc;
  public ExecutableChecker(ClassChecker parentChecker,
      ExecutableMemberDoc doc) {
    super(doc);
    descriptor = parentChecker.getDescriptor() + '.' + 
      doc.name() + doc.flatSignature();
    this.doc = doc;
  }
  protected String getDescriptor() {
    return descriptor;
  }
  public void check() {
    checkComments(); // calls superclass
    checkReturnComments(); // calls subclass
    checkParametersForComments();
    checkExceptionComments();
  }
  public abstract void checkReturnComments();
  private void checkParametersForComments() {
    ParamTag[] tags = doc.paramTags();
    Map tagMap = new HashMap(tags.length);
    for (int i = 0; i < tags.length; i++) {
      if (tagMap.containsKey(tags[i].parameterName()))
        error("parameter \"" + tags[i].parameterName()
            + "\" has multiple comments");
      else if (!isEmpty(tags[i].parameterComment()))
        tagMap.put(tags[i].parameterName(), tags[i]);
    }
    Parameter[] params = doc.parameters();
    for (int i = 0; i < params.length; i++) {
      if (tagMap.remove(params[i].name()) == null
          && !params[i].name().equals("this$0")) {
        error("misses comment for parameter \"" + 
          params[i].name() + "\"");
      }
    }
    Iterator it = tagMap.keySet().iterator();
    while (it.hasNext()) {
      error("parameter \"" + it.next() + "\" does not exist");
    }
  }
  private void checkExceptionComments() {
    ThrowsTag[] tags = doc.throwsTags();
    Map tagMap = new HashMap(tags.length);
    for (int i = 0; i < tags.length; i++) {
      if (tagMap.containsKey(tags[i].exceptionName()))
        error("has multiple comments for exception \"" +
          tags[i].exceptionName() + "\"");
      else if (!isEmpty(tags[i].exceptionComment()))
        tagMap.put(tags[i].exceptionName(), tags[i]);
    }
    ClassDoc[] exceptions = doc.thrownExceptions();
    for (int i = 0; i < exceptions.length; i++) {
      if (tagMap.remove(exceptions[i].name()) == null)
        error("is missing comments for exception \"" +
          exceptions[i].name() + "\"");
    }
    Iterator it = tagMap.keySet().iterator();
    while (it.hasNext()) {
      error("has unnecessary comment for exception \"" +
        it.next() + '"');
    }
  }
  protected void foundCommentsForNonExistentReturnValue() {
    error("has unnecessary return comment");
  }
}

Jetzt haben wir das schlimmste hinter uns. Ooops - sorry - when I am tired I sometimes revert to my mother language ;-) Let's have a look at the checker for the constructors. All we do is check whether there is a tag for @return and if there is, the checker complains.

import com.sun.javadoc.ConstructorDoc;

public class ConstructorChecker extends ExecutableChecker {
  private final ConstructorDoc doc;
  public ConstructorChecker(ClassChecker parent, 
      ConstructorDoc doc) {
    super(parent, doc);
    this.doc = doc;
  }
  public void checkReturnComments() {
    if (doc.tags("return").length > 0)
      foundCommentsForNonExistentReturnValue();
  }
}

The checker for methods is only marginally more complicated than that for constructors:

import com.sun.javadoc.*;

public class MethodChecker extends ExecutableChecker {
  private final MethodDoc doc;
  public MethodChecker(ClassChecker parent, MethodDoc doc) {
    super(parent, doc);
    this.doc = doc;
  }
  public void checkReturnComments() {
    Tag[] tags = doc.tags("return");
    if ("void".equals(doc.returnType().qualifiedTypeName())) {
      if (tags.length != 0) {
        foundCommentsForNonExistentReturnValue();
      }
    } else if (tags.length == 0 || isEmpty(tags[0].text())) {
      error("missing return comment");
    } else if (tags.length > 1) {
      error("has multiple return comments");
    }
  }
}

Lastly, the checker for fields. We don't need to worry about return types, parameters and exceptions, so we simply check that it has a comment at all.

import com.sun.javadoc.FieldDoc;

public class FieldChecker extends Checker {
  private final String descriptor;
  public FieldChecker(ClassChecker parent, FieldDoc doc) {
    super(doc);
    descriptor = parent.getDescriptor() + '.' + doc.name();
  }
  public void check() {
    checkComments();
  }
  protected String getDescriptor() {
    return descriptor;
  }
}

If you stick all these classes in a directory and point JavaDoc onto them, you can use them to check that you have put comments with each important element. What's really nifty is that you can decide at runtime whether to show only public/protected elements or also package private or private.

The way that I use this Doclet is to only release classes once no messages are generated by this CommentChecker. When I change a method significantly, I will generally delete the comment, and then the comment checker will remind me at the next build that I need to add a comment. Because I get reminded to add the comments before I get to release the code, I avoid the pitfall of only adding the comments several months after I wrote the code.

This Doclet has been very helpful to me, in that it made my code look "very professional" (I can't believe I'm saying that ;-).

Attention: A lot of readers ask me whether they are allowed to use the code in my newsletters for their own projects (without paying me). Yes, you may freely use the code in my newsletters (at your sole risk), provided that you have a reference and acknowledgement in your code to my newsletter webpage.

In my next newsletter, I am going to make you scratch your head. I am going to demonstrate that it is possible to make your compiler fail because of what is contained inside a comment.

Until then ...

Heinz

Software Engineering Articles Related Java Course

Java Master
Java Concurrency
Design Patterns
In-House Courses



© 2010-2014 Heinz Kabutz - All Rights Reserved Sitemap
Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners. JavaSpecialists.eu is not connected to Oracle, Inc. and is not sponsored by Oracle, Inc.
@CORE_THE_BAND #RBBJGR