Javadoc¶
You can use CodeQL to find errors in Javadoc comments in Java code.
About analyzing Javadoc¶
To access Javadoc associated with a program element, we use member predicategetDoc
of classElement
, which returns aDocumentable
. ClassDocumentable
, in turn, offers a member predicategetJavadoc
to retrieve the Javadoc attached to the element in question, if any.
Javadoc comments are represented by classJavadoc
, which provides a view of the comment as a tree ofJavadocElement
nodes. EachJavadocElement
is either aJavadocTag
, representing a tag, or aJavadocText
, representing a piece of free-form text.
The most important member predicates of classJavadoc
are:
getAChild
- retrieves a top-levelJavadocElement
node in the tree representation.getVersion
- returns the value of the@version
tag, if any.getAuthor
- returns the value of the@author
tag, if any.
For example, the following query finds all classes that have both an@author
tag and a@version
tag, and returns this information:
importjavafromClassc,Javadocjdoc,stringauthor,stringversionwherejdoc=c.getDoc().getJavadoc()andauthor=jdoc.getAuthor()andversion=jdoc.getVersion()selectc,author,version
JavadocElement
defines member predicatesgetAChild
andgetParent
to navigate up and down the tree of elements. It also provides a predicategetTagName
to return the tag’s name, and a predicategetText
to access the text associated with the tag.
We could rewrite the above query to use this API instead ofgetAuthor
andgetVersion
:
importjavafromClassc,Javadocjdoc,JavadocTagauthorTag,JavadocTagversionTagwherejdoc=c.getDoc().getJavadoc()andauthorTag.getTagName()="@author"andauthorTag.getParent()=jdocandversionTag.getTagName()="@version"andversionTag.getParent()=jdocselectc,authorTag.getText(),versionTag.getText()
TheJavadocTag
has several subclasses representing specific kinds of Javadoc tags:
ParamTag
represents@param
tags; member predicategetParamName
returns the name of the parameter being documented.ThrowsTag
represents@throws
tags; member predicategetExceptionName
returns the name of the exception being documented.AuthorTag
represents@author
tags; member predicategetAuthorName
returns the name of the author.
Example: Finding spurious @param tags¶
As an example of using the CodeQL Javadoc API, let’s write a query that finds@param
tags that refer to a non-existent parameter.
For example, consider this program:
classA{/** * @param lst a list of strings */publicStringget(List<String>list){returnlist.get(0);}}
Here, the@param
tag onA.get
misspells the name of parameterlist
aslst
. Our query should be able to find such cases.
To begin with, we write a query that finds all callables (that is, methods or constructors) and their@param
tags:
importjavafromCallablec,ParamTagptwherec.getDoc().getJavadoc()=pt.getParent()selectc,pt
It’s now easy to add another conjunct to thewhere
clause, restricting the query to@param
tags that refer to a non-existent parameter: we simply need to require that no parameter ofc
has the namept.getParamName()
.
importjavafromCallablec,ParamTagptwherec.getDoc().getJavadoc()=pt.getParent()andnotc.getAParameter().hasName(pt.getParamName())selectpt,"Spurious @param tag."
Example: Finding spurious @throws tags¶
A related, but somewhat more involved, problem is finding@throws
tags that refer to an exception that the method in question cannot actually throw.
For example, consider this Java program:
importjava.io.IOException;classA{/** * @throws IOException thrown if some IO operation fails * @throws RuntimeException thrown if something else goes wrong */publicvoidfoo(){// ...}}
Notice that the Javadoc comment ofA.foo
documents two thrown exceptions:IOException
andRuntimeException
. The former is clearly spurious:A.foo
doesn’t have athrowsIOException
clause, and therefore can’t throw this kind of exception. On the other hand,RuntimeException
is an unchecked exception, so it can be thrown even if there is no explicitthrows
clause listing it. So our query should flag the@throws
tag forIOException
, but not the one forRuntimeException.
Remember that the CodeQL library represents@throws
tags using classThrowsTag
. This class doesn’t provide a member predicate for determining the exception type that is being documented, so we first need to implement our own version. A simple version might look like this:
RefTypegetDocumentedException(ThrowsTagtt){result.hasName(tt.getExceptionName())}
Similarly,Callable
doesn’t come with a member predicate for querying all exceptions that the method or constructor may possibly throw. We can, however, implement this ourselves by usinggetAnException
to find allthrows
clauses of the callable, and then usegetType
to resolve the corresponding exception types:
predicatemayThrow(Callablec,RefTypeexn){exn.getASupertype*()=c.getAnException().getType()}
Note the use ofgetASupertype*
to find both exceptions declared in athrows
clause and their subtypes. For instance, if a method has athrowsIOException
clause, it may throwMalformedURLException
, which is a subtype ofIOException
.
Now we can write a query for finding all callablesc
and@throws
tagstt
such that:
tt
belongs to a Javadoc comment attached toc
.c
can’t throw the exception documented bytt
.
importjava// Insert the definitions from abovefromCallablec,ThrowsTagtt,RefTypeexnwherec.getDoc().getJavadoc()=tt.getParent+()andexn=getDocumentedException(tt)andnotmayThrow(c,exn)selecttt,"Spurious @throws tag."
Improvements¶
Currently, there are two problems with this query:
getDocumentedException
is too liberal: it will returnany reference type with the right name, even if it’s in a different package and not actually visible in the current compilation unit.mayThrow
is too restrictive: it doesn’t account for unchecked exceptions, which do not need to be declared.
To see why the former is a problem, consider this program:
classIOExceptionextendsException{}classB{/** @throws IOException an IO exception */voidbar()throwsIOException{}}
This program defines its own classIOException
, which is unrelated to the classjava.io.IOException
in the standard library: they are in different packages. OurgetDocumentedException
predicate doesn’t check packages, however, so it will consider the@throws
clause to refer to bothIOException
classes, and thus flag the@param
tag as spurious, sinceB.bar
can’t actually throwjava.io.IOException
.
As an example of the second problem, methodA.foo
from our previous example was annotated with a@throwsRuntimeException
tag. Our current version ofmayThrow
, however, would think thatA.foo
can’t throw aRuntimeException
, and thus flag the tag as spurious.
We can makemayThrow
less restrictive by introducing a new class to represent unchecked exceptions, which are just the subtypes ofjava.lang.RuntimeException
andjava.lang.Error
:
classUncheckedExceptionextendsRefType{UncheckedException(){this.getASupertype*().hasQualifiedName("java.lang","RuntimeException")orthis.getASupertype*().hasQualifiedName("java.lang","Error")}}
Now we incorporate this new class into ourmayThrow
predicate:
predicatemayThrow(Callablec,RefTypeexn){exninstanceofUncheckedExceptionorexn.getASupertype*()=c.getAnException().getType()}
FixinggetDocumentedException
is more complicated, but we can easily cover three common cases:
The
@throws
tag specifies the fully qualified name of the exception.The
@throws
tag refers to a type in the same package.The
@throws
tag refers to a type that is imported by the current compilation unit.
The first case can be covered by changinggetDocumentedException
to use the qualified name of the@throws
tag. To handle the second and the third case, we can introduce a new predicatevisibleIn
that checks whether a reference type is visible in a compilation unit, either by virtue of belonging to the same package or by being explicitly imported. We then rewritegetDocumentedException
as:
predicatevisibleIn(CompilationUnitcu,RefTypetp){cu.getPackage()=tp.getPackage()orexists(ImportTypeit|it.getCompilationUnit()=cu|it.getImportedType()=tp)}RefTypegetDocumentedException(ThrowsTagtt){result.getQualifiedName()=tt.getExceptionName()or(result.hasName(tt.getExceptionName())andvisibleIn(tt.getFile(),result))}
This query should find many fewer, more interesting results.
Currently,visibleIn
only considers single-type imports, but you could extend it with support for other kinds of imports.