该库被实现为一个QL模块,是一个后缀为.qll的文件,即java.qll,该模块导入了所有核心Java库模块,因此,我们可以在查询的开始位置通过以下方式来引入完整的java库:
import java
库类总结
标准Java库中重要的类可以分为5种:
- 表示编程元素的类(例如类和方法)
- 表示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代表Java enum类型。
- Array 表示Java数组类型。
例如,以下查询查找程序中所有int类型的变量:
import java from Variable v, PrimitiveType pt where pt = v.getType() and pt.hasName("int") select v
引用类型也根据其声明范围进行分类:
TopLevelType 表示在编译单元的顶层声明的引用类型。
NestedType 是在另一个类型内声明的类型。
例如,此查询查找名称与其编译单元名称不同的所有顶级类型:
import java from TopLevelType tl where tl.getName() != tl.getCompilationUnit().getName() select tl
还提供了更多专业类:
TopLevelClass 表示在编译单元的顶层声明的类。
NestedClass表示在另一种类型内声明的类,例如:
LocalClass,在方法或构造函数内部声明的类。
AnonymousClass,匿名类。
最后,CodeQL java库也有一些单例类包装了常用的Java标准库类,如:TypeObject,TypeCloneable,TypeRuntime,TypeSerializable,TypeString,TypeSystem和TypeClass。每个CodeQL类代表其类名所建议的标准Java类。
例如,我们可以编写一个查询来查找所有直接继承自Object的嵌套类:
import java from NestedClass nc where nc.getASupertype() instanceof TypeObject select nc
泛型
还有一些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。我们说N是一个有界类型参数,Number是其上限。在QL中,可以使用谓词getATypeBound来查询类型变量的类型界限。类型范围本身由TypeBound类表示,该类通过一个成员谓词getType来检索变量所限制的类型。
例如,以下查询查找所有限制为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的角度看Variable类表示一个变量,它可以是类的成员变量(无论是静态的还是非静态的),或者是局部变量或参数。因此,有三个子类可以满足这些特殊情况:
- Field 表示一个Java字段。
- LocalVariableDecl 代表局部变量。
- Parameter 表示方法或构造函数的参数。
抽象语法树
这个类别中的类表示抽象语法树(AST)节点,即语句(Stmt类)和表达式(Expr类)。有关标准QL库中可用的表达式和语句类型的完整列表,请参见用于Java程序的抽象语法树类。
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 x + y;,则此查询将返回x + y
另一个示例,以下查询查找其父节点为if语句的所有语句:
import java from Stmt s where s.getParent() instanceof IfStmt select s
该查询将找到程序中所有if语句的then分支和else分支。
最后一个示例,这是一个查找方法主体的查询:
import java from Stmt s where s.getParent() instanceof Method select s
正如这些示例所示,表达式的父节点并不总是一个表达式:它也可以是一条语句,例如 IfStmt。同样,一条语句的父节点并不总是一条语句:它也可以是方法或构造函数。为了捕获这一点,QL Java库提供了两个抽象类ExprParent和StmtParent,前者表示可能是表达式的父节点的任何节点,而后者则表示可能是语句的父节点的任何节点。
元数据
Java程序除了程序代码外,还具有几种元数据。特别是有注解和Javadoc注释。由于这些元数据对于增强代码分析以及本身作为一个分析主题都很有意思,所以QL库定义了用于访问它的类。
对于注解,Annotatable类是所有可以注解的程序元素的超类。这包括包,引用类型,字段,方法,构造函数和局部变量声明。对于每个这样的元素,其谓词getAnAnnotation可以检索任何该元素可能具有的注解。例如,以下查询用于查找构造函数的所有注解:
import java from Constructor c select c.getAnAnnotation()
这些注解由Annotation类表示。一个注解只是类型为AnnotationType的一个简单表达式。例如,您可以修改上面的查询,以使其仅报告Deprecated注解的构造函数:
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
有关使用注解的更多信息,请参阅关于注解的文章。
对于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代码库生成的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