CodeQL 文档

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 库中最重要的类可以分为五个主要类别

  1. 用于表示程序元素(例如类和方法)的类
  2. 用于表示 AST 节点(例如语句和表达式)的类
  3. 用于表示元数据(例如注解和注释)的类
  4. 用于计算指标(例如圈复杂度和耦合)的类
  5. 用于遍历程序调用图的类

我们将依次讨论这些内容,简要描述每个类别中最重要的类。

程序元素

这些类表示命名的程序元素:包 (Package)、编译单元 (CompilationUnit)、类型 (Type)、方法 (Method)、构造函数 (Constructor) 和变量 (Variable)。

它们的共同超类是 Element,它提供用于确定程序元素名称和检查两个元素是否相互嵌套的一般成员谓词。

通常,引用可能为方法或构造函数的元素很方便;类 CallableMethodConstructor 的共同超类,可用于此目的。

类型

Type 具有许多子类,用于表示不同类型的类型

  • PrimitiveType 表示一种 基本类型,即 booleanbytechardoublefloatintlongshort 之一;QL 还将 void<nulltype>null 字面量的类型)归类为基本类型。
  • RefType 表示引用(即非基本)类型;它反过来也有几个子类
    • Class 表示 Java 类。
    • Interface 表示 Java 接口。
    • EnumType 表示 Java enum 类型。
    • 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

您通常会在存储库的源代码中看到这种模式,在源代码引用的文件中,会有更多实例。

还提供了一些更专业的类

最后,库中还有一些单例类,它们包装了常用的 Java 标准库类:TypeObjectTypeCloneableTypeRuntimeTypeSerializableTypeStringTypeSystemTypeClass。每个 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();

    // ...
}

类型参数,例如此示例中的 KV,由类 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 表示,它具有预期的子类 RawClassRawInterface。同样,有一个谓词 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 提供成员谓词 getUpperBoundgetLowerBound 分别用于检索上限和下限。

为了处理泛型方法,有类 GenericMethodParameterizedMethodRawMethod,它们与用于表示泛型类型的同名类完全类似。

有关使用类型的更多信息,请参见 Java 和 Kotlin 中的类型

变量

Variable 表示 Java 意义上的 变量,它可以是类的成员字段(无论是静态的还是非静态的),也可以是局部变量或参数。因此,有三个子类分别满足这些特殊情况

  • Field 表示 Java 字段。
  • LocalVariableDecl 表示局部变量。
  • Parameter 表示方法或构造函数的参数。

抽象语法树

此类别中的类表示抽象语法树 (AST) 节点,即语句(类 Stmt)和表达式(类 Expr)。有关标准 QL 库中提供的表达式和语句类型的完整列表,请参见“用于处理 Java 和 Kotlin 程序的抽象语法树类”。

ExprStmt 都提供成员谓词来探索程序的抽象语法树

  • Expr.getAChildExpr 返回给定表达式的子表达式。
  • Stmt.getAChild 返回直接嵌套在给定语句中的语句或表达式。
  • Expr.getParentStmt.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 库提供了两个抽象类 ExprParentStmtParent,前者表示任何可能成为表达式父节点的节点,后者表示任何可能成为语句父节点的节点。

有关使用 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 节点的树,可以使用成员谓词 getAChildgetParent 遍历它们。例如,您可以编辑查询,使其找到私有字段上的 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 程序元素的指标提供了广泛的支持。为了避免用太多与度量计算相关的成员谓词来压垮表示这些元素的类,这些谓词是在委托类上提供的。

总共有六个这样的类:MetricElementMetricPackageMetricRefTypeMetricFieldMetricCallableMetricStmt。相应的元素类都提供一个成员谓词 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 表达式以及使用 thissuper 的显式构造函数调用。

我们可以使用谓词 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

代码库通常有许多未直接调用的方法,但这可能并非全部。要进一步探索该领域,请参见“导航调用图”。

有关可调用项和调用的更多信息,请参见 关于调用图的文章

  • ©GitHub, Inc.
  • 条款
  • 隐私