The Java Specialists' Newsletter [Issue 049] - Doclet for finding
missing comments
Author: Dr. Heinz M. Kabutz
JDK version:
Category: Software Engineering
You can subscribe from our home page:
http://www.javaspecialists.co.za (which also hosts all previous issues,
available free of charge
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 "The Shuttleworth Foundation (TSF)".
In the unlikely case that you are unfamiliar with the name "Shuttleworth", it
belongs to the first 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. TSF is a non-profit organization aimed at
providing help to schools, for example, a school administration tool.
Some of my readers mentioned after
newsletter 047 that
they could not find my travel report of Mauritius. Please have a look at
http://www.javaspecialists.co.za/designpatterns/mauritius.html for a
direct link.
Times are tough, I don't know if you're feeling it as well? When I started
contracting, I went for over 3 years without a single day when I didn't have
programming or training work. All of a sudden, I found myself pottering around
for two weeks at a time, scratching my head wondering where the next project
would come from. Fortunately, I have put my time to good use and developed some
courses, which have
done extremely well. However, I'm not programming that much anymore, and
that upsets me greatly. I feel far happier when I'm working on a program than if
I have to run around trying to sell my courseware. It is not even that my hourly
rate for programming is excessively high. It's not. I would love to hear from
you if you have a project that could do with some outside help from a Java
specialist, especially if it can be done remotely.
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
http://www.javaspecialists.co.za.
However, if my newsletters have really helped you or you are using code
from my newsletters (especially this one), you can gladly make a donation to
Feeding the 5000
a non-profit organisation for providing food and clothing to the really really
poor in the Helderberg, the area of South Africa where I live.
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
Copyright 2000-2004 Maximum Solutions, South Africa
Reprint Rights. Copyright subsists in all the material included
in this email, but you may freely share the entire email with anyone you feel
may be interested, and you may reprint excerpts both online and offline provided
that you acknowledge the source as follows: This material from The Java(tm)
Specialists' Newsletter by Maximum Solutions (South Africa). Please contact
Maximum Solutions for more
information.
Java and Sun are trademarks or registered trademarks of Sun Microsystems,
Inc. in the United States and other countries. Maximum Solutions is independent
of Sun Microsystems, Inc.
20276 bytes more | comments? | | Score: 0
|