Javadoc¶
您可以使用 CodeQL 查找 Java 代码中 Javadoc 注释中的错误。
关于分析 Javadoc¶
要访问与程序元素关联的 Javadoc,我们使用类 Element
的成员谓词 getDoc
,它返回一个 Documentable
。类 Documentable
又提供了一个成员谓词 getJavadoc
,用于检索与该元素关联的 Javadoc(如果有)。
Javadoc 注释由类 Javadoc
表示,它提供对注释的视图,该注释作为 JavadocElement
节点的树。每个 JavadocElement
都是 JavadocTag
(表示标签)或 JavadocText
(表示一段自由格式文本)。
类 Javadoc
最重要的成员谓词是
getAChild
- 在树表示中检索顶层JavadocElement
节点。getVersion
- 返回@version
标签的值(如果有)。getAuthor
- 返回@author
标签的值(如果有)。
例如,以下查询查找所有同时具有 @author
标签和 @version
标签的类,并返回此信息
import java
from Class c, Javadoc jdoc, string author, string version
where jdoc = c.getDoc().getJavadoc() and
author = jdoc.getAuthor() and
version = jdoc.getVersion()
select c, author, version
JavadocElement
定义了成员谓词 getAChild
和 getParent
,用于向上和向下导航元素树。它还提供了一个谓词 getTagName
来返回标签的名称,以及一个谓词 getText
来访问与标签关联的文本。
我们可以使用此 API 重新编写上述查询,而不是使用 getAuthor
和 getVersion
import java
from Class c, Javadoc jdoc, JavadocTag authorTag, JavadocTag versionTag
where jdoc = c.getDoc().getJavadoc() and
authorTag.getTagName() = "@author" and authorTag.getParent() = jdoc and
versionTag.getTagName() = "@version" and versionTag.getParent() = jdoc
select c, authorTag.getText(), versionTag.getText()
JavadocTag
有几个子类,代表特定类型的 Javadoc 标签
ParamTag
代表@param
标签;成员谓词getParamName
返回正在记录的参数的名称。ThrowsTag
代表@throws
标签;成员谓词getExceptionName
返回正在记录的异常的名称。AuthorTag
代表@author
标签;成员谓词getAuthorName
返回作者的姓名。
示例:查找虚假的 @param 标签¶
作为使用 CodeQL Javadoc API 的示例,让我们编写一个查找引用不存在参数的 @param
标签的查询。
例如,请考虑以下程序
class A {
/**
* @param lst a list of strings
*/
public String get(List<String> list) {
return list.get(0);
}
}
在此,A.get
上的 @param
标签将参数 list
的名称拼写错误为 lst
。我们的查询应该能够找到此类情况。
首先,我们编写一个查询,查找所有可调用项(即方法或构造函数)及其 @param
标签
import java
from Callable c, ParamTag pt
where c.getDoc().getJavadoc() = pt.getParent()
select c, pt
现在可以轻松地将另一个合取词添加到 where
子句中,将查询限制为引用不存在参数的 @param
标签:我们只需要求 c
的任何参数都没有名称 pt.getParamName()
。
import java
from Callable c, ParamTag pt
where c.getDoc().getJavadoc() = pt.getParent() and
not c.getAParameter().hasName(pt.getParamName())
select pt, "Spurious @param tag."
示例:查找虚假的 @throws 标签¶
一个相关但稍微复杂一点的问题是查找引用该方法实际上无法抛出的异常的 @throws
标签。
例如,请考虑以下 Java 程序
import java.io.IOException;
class A {
/**
* @throws IOException thrown if some IO operation fails
* @throws RuntimeException thrown if something else goes wrong
*/
public void foo() {
// ...
}
}
请注意,A.foo
的 Javadoc 注释记录了两个抛出的异常:IOException
和 RuntimeException
。前者显然是虚假的:A.foo
没有 throws IOException
子句,因此无法抛出此类异常。另一方面,RuntimeException
是一个未经检查的异常,因此即使没有明确列出它的 throws
子句,它也可以抛出。因此,我们的查询应该标记 IOException
的 @throws
标签,但不标记 RuntimeException
的标签。
请记住,CodeQL 库使用类 ThrowsTag
来表示 @throws
标签。此类不提供成员谓词来确定正在记录的异常类型,因此我们首先需要实现自己的版本。一个简单的版本可能如下所示
RefType getDocumentedException(ThrowsTag tt) {
result.hasName(tt.getExceptionName())
}
类似地,Callable
没有附带用于查询方法或构造函数可能抛出的所有异常的成员谓词。但是,我们可以使用 getAnException
找到可调用项的所有 throws
子句,然后使用 getType
来解析相应的异常类型,从而自行实现此功能
predicate mayThrow(Callable c, RefType exn) {
exn.getASupertype*() = c.getAnException().getType()
}
请注意,使用 getASupertype*
来查找在 throws
子句中声明的异常及其子类型。例如,如果一个方法具有 throws IOException
子句,则它可能会抛出 MalformedURLException
(它是 IOException
的子类型)。
现在,我们可以编写一个查询,用于查找所有可调用项 c
和 @throws
标签 tt
,使
tt
属于附加到c
的 Javadoc 注释。c
无法抛出tt
记录的异常。
import java
// Insert the definitions from above
from Callable c, ThrowsTag tt, RefType exn
where c.getDoc().getJavadoc() = tt.getParent+() and
exn = getDocumentedException(tt) and
not mayThrow(c, exn)
select tt, "Spurious @throws tag."
改进¶
目前,此查询有两个问题
getDocumentedException
太宽松:它将返回具有正确名称的任何引用类型,即使它在不同的包中,并且实际上在当前编译单元中不可见。mayThrow
太严格:它没有考虑未经检查的异常,这些异常不需要声明。
要了解为什么前者是一个问题,请考虑以下程序
class IOException extends Exception {}
class B {
/** @throws IOException an IO exception */
void bar() throws IOException {}
}
此程序定义了自己的类 IOException
,它与标准库中的类 java.io.IOException
无关:它们位于不同的包中。但是,我们的 getDocumentedException
谓词不会检查包,因此它将认为 @throws
子句引用了两个 IOException
类,因此将 @param
标签标记为虚假,因为 B.bar
实际上无法抛出 java.io.IOException
。
作为第二个问题的示例,我们之前示例中的方法 A.foo
带有 @throws RuntimeException
标签。但是,我们当前版本的 mayThrow
会认为 A.foo
无法抛出 RuntimeException
,因此会将该标签标记为虚假。
我们可以通过引入一个新类来表示未经检查的异常(它们只是 java.lang.RuntimeException
和 java.lang.Error
的子类型)来使 mayThrow
变得不那么严格
class UncheckedException extends RefType {
UncheckedException() {
this.getASupertype*().hasQualifiedName("java.lang", "RuntimeException") or
this.getASupertype*().hasQualifiedName("java.lang", "Error")
}
}
现在,我们将此新类合并到我们的 mayThrow
谓词中
predicate mayThrow(Callable c, RefType exn) {
exn instanceof UncheckedException or
exn.getASupertype*() = c.getAnException().getType()
}
修复 getDocumentedException
更加复杂,但我们可以轻松地涵盖三个常见情况
@throws
标签指定异常的完全限定名称。@throws
标签引用同一个包中的类型。@throws
标签引用当前编译单元导入的类型。
第一种情况可以通过修改 getDocumentedException
来使用 @throws
标签的限定名称来解决。为了处理第二种和第三种情况,我们可以引入一个新的谓词 visibleIn
,它检查引用类型在编译单元中是否可见,无论是通过属于同一个包,还是通过显式导入。然后我们将 getDocumentedException
重写为
predicate visibleIn(CompilationUnit cu, RefType tp) {
cu.getPackage() = tp.getPackage()
or
exists(ImportType it | it.getCompilationUnit() = cu | it.getImportedType() = tp)
}
RefType getDocumentedException(ThrowsTag tt) {
result.getQualifiedName() = tt.getExceptionName()
or
(result.hasName(tt.getExceptionName()) and visibleIn(tt.getFile(), result))
}
此查询应该找到更少、更有意思的结果。
目前,visibleIn
仅考虑单类型导入,但您可以扩展它以支持其他类型的导入。