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 仅考虑单类型导入,但您可以扩展它以支持其他类型的导入。