|
The Java Specialists' Newsletter
Issue 049 2002-06-05
Category:
Software Engineering
Java version: Doclet for finding missing commentsby 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.
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?
-
The person who has to maintain my code may not share my enthusiasm
for reading code in order to understand what I was doing.
-
I quite like explaining in my comments the why of what I was
doing.
-
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
Discuss at The Java Specialist Club
|