Abstract: Another use of JavaDoc is to use the engine to find where comments are missing from our code.
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.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
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?
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
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.