Java 和 Kotlin 的 CodeQL 库¶
当您分析 Java/Kotlin 程序时,您可以使用 Java/Kotlin 的 CodeQL 库中的大量类。
注意
Kotlin 的 CodeQL 分析目前处于测试阶段。在测试期间,对 Kotlin 代码的分析以及相关文档不会像其他语言那样全面。
关于 Java 和 Kotlin 的 CodeQL 库¶
有一个广泛的库用于分析从 Java/Kotlin 项目中提取的 CodeQL 数据库。该库中的类以面向对象的形式呈现数据库中的数据,并提供抽象和谓词来帮助您完成常见的分析任务。
该库实现为一组 QL 模块,即扩展名为 .qll
的文件。模块 java.qll
导入所有核心 Java 库模块,因此您可以通过在查询开头包含以下内容来包含整个库
import java
本文其余部分将简要总结该库提供的最重要的类和谓词。
注意
本文中的示例查询说明了不同库类返回的结果类型。结果本身并不重要,但可以作为开发更复杂查询的基础。本节中其他文章展示了如何采用简单的查询并对其进行微调以精确找到您感兴趣的结果。
库类摘要¶
标准 Java/Kotlin 库中最重要的类可以分为五个主要类别
- 用于表示程序元素(例如类和方法)的类
- 用于表示 AST 节点(例如语句和表达式)的类
- 用于表示元数据(例如注解和注释)的类
- 用于计算指标(例如圈复杂度和耦合)的类
- 用于遍历程序调用图的类
我们将依次讨论这些内容,简要描述每个类别中最重要的类。
程序元素¶
这些类表示命名的程序元素:包 (Package
)、编译单元 (CompilationUnit
)、类型 (Type
)、方法 (Method
)、构造函数 (Constructor
) 和变量 (Variable
)。
它们的共同超类是 Element
,它提供用于确定程序元素名称和检查两个元素是否相互嵌套的一般成员谓词。
通常,引用可能为方法或构造函数的元素很方便;类 Callable
是 Method
和 Constructor
的共同超类,可用于此目的。
类型¶
类 Type
具有许多子类,用于表示不同类型的类型
PrimitiveType
表示一种 基本类型,即boolean
、byte
、char
、double
、float
、int
、long
、short
之一;QL 还将void
和<nulltype>
(null
字面量的类型)归类为基本类型。RefType
表示引用(即非基本)类型;它反过来也有几个子类Class
表示 Java 类。Interface
表示 Java 接口。EnumType
表示 Javaenum
类型。Array
表示 Java 数组类型。
例如,以下查询查找程序中所有类型为 int
的变量
import java
from Variable v, PrimitiveType pt
where pt = v.getType() and
pt.hasName("int")
select v
当您运行此查询时,您可能会得到很多结果,因为大多数项目都包含许多类型为 int
的变量。
引用类型也根据其声明范围进行分类
TopLevelType
表示在编译单元的顶层声明的引用类型。NestedType
是在另一个类型中声明的类型。
例如,此查询查找名称与其编译单元不同的所有顶层类型
import java
from TopLevelType tl
where tl.getName() != tl.getCompilationUnit().getName()
select tl
您通常会在存储库的源代码中看到这种模式,在源代码引用的文件中,会有更多实例。
还提供了一些更专业的类
TopLevelClass
表示在编译单元的顶层声明的类。NestedClass
表示 在另一个类型中声明的类,例如LocalClass
,它是在 方法或构造函数中声明的类。AnonymousClass
,它是一个 匿名类。
最后,库中还有一些单例类,它们包装了常用的 Java 标准库类:TypeObject
、TypeCloneable
、TypeRuntime
、TypeSerializable
、TypeString
、TypeSystem
和 TypeClass
。每个 CodeQL 类都表示其名称建议的标准 Java 类。
例如,我们可以编写一个查询,查找所有直接扩展 Object
的嵌套类
import java
from NestedClass nc
where nc.getASupertype() instanceof TypeObject
select nc
当您运行此查询时,您可能会得到很多结果,因为许多项目都包含直接扩展 Object
的嵌套类。
泛型¶
也有几个 Type
的子类用于处理泛型类型。
GenericType
既可以是 GenericInterface
,也可以是 GenericClass
。它表示泛型类型声明,例如来自 Java 标准库的接口 java.util.Map
package java.util.;
public interface Map<K, V> {
int size();
// ...
}
类型参数,例如此示例中的 K
和 V
,由类 TypeVariable
表示。
泛型类型的参数化实例提供了一个具体的类型,用于实例化类型参数,例如 Map<String, File>
。这种类型由 ParameterizedType
表示,它与表示它被实例化的泛型类型的 GenericType
不同。要从 ParameterizedType
转到其相应的 GenericType
,您可以使用谓词 getSourceDeclaration
。
例如,我们可以使用以下查询查找 java.util.Map
的所有参数化实例
import java
from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
pt.getSourceDeclaration() = map
select pt
通常,泛型类型可能会限制类型参数可以绑定到的类型。例如,从字符串到数字的映射类型可以声明如下
class StringToNumMap<N extends Number> implements Map<String, N> {
// ...
}
这意味着 StringToNumberMap
的参数化实例只能用 Number
或其子类型之一实例化类型参数 N
,而不能用 File
实例化。
例如,以下查询查找所有类型绑定为 Number
的类型变量
import java
from TypeVariable tv, TypeBound tb
where tb = tv.getATypeBound() and
tb.getType().hasQualifiedName("java.lang", "Number")
select tv
为了处理不知道泛型的遗留代码,每个泛型类型都有一个没有类型参数的“原始”版本。在 CodeQL 库中,原始类型使用类 RawType
表示,它具有预期的子类 RawClass
和 RawInterface
。同样,有一个谓词 getSourceDeclaration
用于获取相应的泛型类型。例如,我们可以查找类型为 Map
的变量
import java
from Variable v, RawType rt
where rt = v.getType() and
rt.getSourceDeclaration().hasQualifiedName("java.util", "Map")
select v
例如,在以下代码片段中,此查询将找到 m1
,但不会找到 m2
Map m1 = new HashMap();
Map<String, String> m2 = new HashMap<String, String>();
最后,可以将变量声明为 通配符类型
Map<? extends Number, ? super Float> m;
通配符 ? extends Number
和 ? super Float
由类 WildcardTypeAccess
表示。与类型参数类似,通配符可能具有类型边界。与类型参数不同,通配符可以有上限(如 ? extends Number
),也可以有下限(如 ? super Float
)。类 WildcardTypeAccess
提供成员谓词 getUpperBound
和 getLowerBound
分别用于检索上限和下限。
为了处理泛型方法,有类 GenericMethod
、ParameterizedMethod
和 RawMethod
,它们与用于表示泛型类型的同名类完全类似。
有关使用类型的更多信息,请参见 Java 和 Kotlin 中的类型。
抽象语法树¶
此类别中的类表示抽象语法树 (AST) 节点,即语句(类 Stmt
)和表达式(类 Expr
)。有关标准 QL 库中提供的表达式和语句类型的完整列表,请参见“用于处理 Java 和 Kotlin 程序的抽象语法树类”。
Expr
和 Stmt
都提供成员谓词来探索程序的抽象语法树
Expr.getAChildExpr
返回给定表达式的子表达式。Stmt.getAChild
返回直接嵌套在给定语句中的语句或表达式。Expr.getParent
和Stmt.getParent
返回 AST 节点的父节点。
例如,以下查询查找所有其父节点为 return
语句的表达式
import java
from Expr e
where e.getParent() instanceof ReturnStmt
select e
许多项目都有带子表达式的 return
语句的示例。
因此,如果程序包含一个 return 语句 return x + y;
,则此查询将返回 x + y
。
另一个示例,以下查询查找其父节点为 if
语句的语句
import java
from Stmt s
where s.getParent() instanceof IfStmt
select s
许多项目都有带子语句的 if
语句的示例。
此查询将找到程序中所有 if
语句的 then
分支和 else
分支。
最后,这是一个查找方法体的查询
import java
from Stmt s
where s.getParent() instanceof Method
select s
正如这些示例所示,表达式的父节点并不总是表达式:它也可能是一个语句,例如 IfStmt
。类似地,语句的父节点并不总是语句:它也可能是一个方法或一个构造函数。为了捕获这一点,QL Java 库提供了两个抽象类 ExprParent
和 StmtParent
,前者表示任何可能成为表达式父节点的节点,后者表示任何可能成为语句父节点的节点。
有关使用 AST 类的更多信息,请参见 有关 Java 和 Kotlin 中溢出易发比较的文章。
元数据¶
除了程序代码本身之外,Java/Kotlin 程序还具有几种元数据。特别地,有 注释 和 Javadoc 注释。由于此元数据对于增强代码分析以及作为其自身分析主题都很有趣,因此 QL 库定义了用于访问它的类。
对于注释,类 Annotatable
是所有可以注释的程序元素的超类。这包括包、引用类型、字段、方法、构造函数和局部变量声明。对于每个这样的元素,其谓词 getAnAnnotation
允许您检索元素可能具有的任何注释。例如,以下查询查找构造函数上的所有注释
import java
from Constructor c
select c.getAnAnnotation()
您可能会看到注释用于抑制警告或将代码标记为已弃用的示例。
这些注释由类 Annotation
表示。注释只是一个表达式,其类型是 AnnotationType
。例如,您可以修改此查询,使其仅报告已弃用的构造函数
import java
from Constructor c, Annotation ann, AnnotationType anntp
where ann = c.getAnAnnotation() and
anntp = ann.getType() and
anntp.hasQualifiedName("java.lang", "Deprecated")
select ann
这次只报告具有 @Deprecated
注释的构造函数。
有关使用注释的更多信息,请参见 有关注释的文章。
对于 Javadoc,类 Element
具有一个成员谓词 getDoc
,它返回一个委托 Documentable
对象,然后可以查询它附带的 Javadoc 注释。例如,以下查询查找私有字段上的 Javadoc 注释
import java
from Field f, Javadoc jdoc
where f.isPrivate() and
jdoc = f.getDoc().getJavadoc()
select jdoc
您可以在许多项目中看到这种模式。
类 Javadoc
将整个 Javadoc 注释表示为 JavadocElement
节点的树,可以使用成员谓词 getAChild
和 getParent
遍历它们。例如,您可以编辑查询,使其找到私有字段上的 Javadoc 注释中的所有 @author
标签
import java
from Field f, Javadoc jdoc, AuthorTag at
where f.isPrivate() and
jdoc = f.getDoc().getJavadoc() and
at.getParent+() = jdoc
select at
注意
在第 5 行,我们使用了
getParent+
来捕获嵌套在 Javadoc 注释中任何深度的标签。
有关使用 Javadoc 的更多信息,请参见 有关 Javadoc 的文章。
指标¶
标准 QL Java 库为计算 Java 程序元素的指标提供了广泛的支持。为了避免用太多与度量计算相关的成员谓词来压垮表示这些元素的类,这些谓词是在委托类上提供的。
总共有六个这样的类:MetricElement
、MetricPackage
、MetricRefType
、MetricField
、MetricCallable
和 MetricStmt
。相应的元素类都提供一个成员谓词 getMetrics
,可用于获取委托类的实例,然后可以在该实例上执行度量计算。
例如,以下查询查找 循环复杂度 大于 40 的方法
import java
from Method m, MetricCallable mc
where mc = m.getMetrics() and
mc.getCyclomaticComplexity() > 40
select m
大多数大型项目都包含一些循环复杂度非常高的方法。这些方法可能难以理解和测试。
调用图¶
从 Java 和 Kotlin 代码库生成的 CodeQL 数据库包含有关程序调用图的预计算信息,即给定调用在运行时可能分派到哪些方法或构造函数。
上面介绍的类 Callable
包括方法和构造函数。调用表达式使用类 Call
抽象化,该类包括方法调用、new
表达式以及使用 this
或 super
的显式构造函数调用。
我们可以使用谓词 Call.getCallee
来找出特定调用表达式引用了哪个方法或构造函数。例如,以下查询查找对名为 println
的方法的所有调用
import java
from Call c, Method m
where m = c.getCallee() and
m.hasName("println")
select c
相反,Callable.getAReference
返回一个引用它的 Call
。因此,我们可以使用此查询查找从未调用的方法和构造函数
import java
from Callable c
where not exists(c.getAReference())
select c
代码库通常有许多未直接调用的方法,但这可能并非全部。要进一步探索该领域,请参见“导航调用图”。
有关可调用项和调用的更多信息,请参见 关于调用图的文章。