Java 和 Kotlin 中的类型¶
您可以使用 CodeQL 查找有关 Java/Kotlin 代码中使用的数据类型的信息。这使您可以编写查询以识别特定类型的相关问题。
注意
目前,适用于 Kotlin 的 CodeQL 分析处于测试阶段。在测试期间,对 Kotlin 代码的分析以及相关文档将不会像其他语言那样全面。
关于处理 Java 类型¶
标准 CodeQL 库通过 Type 类及其各种子类来表示 Java 类型。
特别是,PrimitiveType 类表示内置于 Java 语言的原始类型(如 boolean 和 int),而 RefType 及其子类则表示引用类型,即类、接口、数组类型等等。这包括 Java 标准库中的类型(如 java.lang.Object)和非库代码定义的类型。
RefType 类还对类层次结构进行建模:成员谓词 getASupertype 和 getASubtype 允许您查找引用类型的直接超类型和子类型。例如,考虑以下 Java 程序
class A {}
interface I {}
class B extends A implements I {}
在此,A 类只有一个直接超类型(java.lang.Object)和一个直接子类型(B);I 接口也是如此。另一方面,B 类有两个直接超类型(A 和 I),且没有直接子类型。
为了确定祖先类型(包括直接超类型,以及它们的超类型等等),我们可以使用传递闭包。例如,要查找上面示例中 B 的所有祖先,我们可以使用以下查询
import java
from Class B
where B.hasName("B")
select B.getASupertype+()
如果我们在上面的示例代码片段上运行此查询,则查询将返回 A、I 和 java.lang.Object。
提示
如果您想查看
B以及A的位置,可以将B.getASupertype+()替换为B.getASupertype*()并重新运行查询。
除了类层次结构建模外,RefType 还提供成员谓词 getAMember 用于访问在类型中声明的成员(即字段、构造函数和方法),以及谓词 inherits(Method m) 用于检查类型是否声明或继承了方法 m。
示例:查找有问题的数组强制转换¶
作为如何使用类层次结构 API 的示例,我们可以编写一个查询来查找数组的向下强制转换,即一些类型为 A[] 的表达式 e 被转换为类型 B[] 的情况,其中 B 是 A 的(不一定直接的)子类型。
这种强制转换是有问题的,因为向下强制转换数组会导致运行时异常,即使每个单独的数组元素都可以向下强制转换。例如,以下代码会抛出 ClassCastException
Object[] o = new Object[] { "Hello", "world" };
String[] s = (String[])o;
另一方面,如果表达式 e 碰巧实际上计算为一个 B[] 数组,则强制转换将成功
Object[] o = new String[] { "Hello", "world" };
String[] s = (String[])o;
在本教程中,我们不会尝试区分这两种情况。我们的查询应该简单地查找从某类型 source 强制转换为另一类型 target 的强制转换表达式 ce,其中
source和target都是数组类型。source的元素类型是target的元素类型的传递超类型。
将此食谱转换为查询并不太难
import java
from CastExpr ce, Array source, Array target
where source = ce.getExpr().getType() and
target = ce.getType() and
target.getElementType().(RefType).getASupertype+() = source.getElementType()
select ce, "Potentially problematic array downcast."
许多项目会为此查询返回结果。
请注意,通过将 target.getElementType() 强制转换为 RefType,我们消除了元素类型为原始类型的情况,即 target 是原始类型数组:我们正在寻找的问题在这种情况下不会出现。与 Java 不同,QL 中的强制转换永远不会失败:如果表达式无法强制转换为所需类型,它将简单地从查询结果中排除,这正是我们想要的结果。
改进¶
在版本 5 之前,在旧的 Java 代码上运行此查询通常会返回许多由于使用 Collection.toArray(T[]) 方法而产生的误报结果,该方法将集合转换为类型为 T[] 的数组。
在不使用泛型的代码中,此方法通常以以下方式使用
List l = new ArrayList();
// add some elements of type A to l
A[] as = (A[])l.toArray(new A[0]);
在这里,l 的原始类型为 List,因此 l.toArray 的返回类型为 Object[],与它的参数数组的类型无关。因此,强制转换从 Object[] 到 A[],并且我们的查询会将其标记为有问题的,尽管在运行时这种强制转换永远不会出错。
为了识别这些情况,我们可以创建两个 CodeQL 类,分别表示 Collection.toArray 方法以及对该方法或任何覆盖它的方法的调用
/** class representing java.util.Collection.toArray(T[]) */
class CollectionToArray extends Method {
CollectionToArray() {
this.getDeclaringType().hasQualifiedName("java.util", "Collection") and
this.hasName("toArray") and
this.getNumberOfParameters() = 1
}
}
/** class representing calls to java.util.Collection.toArray(T[]) */
class CollectionToArrayCall extends MethodAccess {
CollectionToArrayCall() {
exists(CollectionToArray m |
this.getMethod().getSourceDeclaration().overridesOrInstantiates*(m)
)
}
/** the call's actual return type, as determined from its argument */
Array getActualReturnType() {
result = this.getArgument(0).getType()
}
}
请注意,在 CollectionToArrayCall 的构造函数中使用 getSourceDeclaration 和 overridesOrInstantiates:我们想要找到对 Collection.toArray 以及任何覆盖它的方法的调用,以及这些方法的任何参数化实例。例如,在我们上面的示例中,调用 l.toArray 解析为原始类 ArrayList 中的 toArray 方法。它的源声明是泛型类 ArrayList<T> 中的 toArray,它覆盖了 AbstractCollection<T>.toArray,而 AbstractCollection<T>.toArray 又覆盖了 Collection<T>.toArray,它是 Collection.toArray 的实例化(因为被覆盖方法中的类型参数 T 属于 ArrayList 并且是属于 Collection 的类型参数的实例化)。
使用这些新类,我们可以扩展我们的查询以排除对类型为 A[] 的参数的 toArray 的调用,然后将其强制转换为 A[]
import java
// Insert the class definitions from above
from CastExpr ce, Array source, Array target
where source = ce.getExpr().getType() and
target = ce.getType() and
target.getElementType().(RefType).getASupertype+() = source.getElementType() and
not ce.getExpr().(CollectionToArrayCall).getActualReturnType() = target
select ce, "Potentially problematic array downcast."
请注意,此改进的查询找到的结果更少。
示例:查找不匹配的包含检查¶
我们现在将开发一个查询来查找 Collection.contains 的用法,其中查询元素的类型与集合的元素类型无关,这保证测试将始终返回 false。
例如,Apache Zookeeper 过去在 QuorumPeerConfig 类中有一段类似于以下内容的代码
Map<Object, Object> zkProp;
// ...
if (zkProp.entrySet().contains("dynamicConfigFile")){
// ...
}
由于 zkProp 是从 Object 到 Object 的映射,因此 zkProp.entrySet 返回类型为 Set<Entry<Object, Object>> 的集合。这样的集合不可能包含类型为 String 的元素。(该代码现已修复为使用 zkProp.containsKey。)
一般来说,我们想要找到对 Collection.contains(或其在 Collection 的任何参数化实例中覆盖的任何方法)的调用,其中集合元素的类型 E 和 contains 参数的类型 A 是无关的,即它们没有共同的子类型。
我们首先创建一个描述 java.util.Collection 的类
class JavaUtilCollection extends GenericInterface {
JavaUtilCollection() {
this.hasQualifiedName("java.util", "Collection")
}
}
为了确保我们没有打错字,我们可以运行一个简单的测试查询
from JavaUtilCollection juc
select juc
此查询应该返回正好一个结果。
接下来,我们可以创建一个描述 java.util.Collection.contains 的类
class JavaUtilCollectionContains extends Method {
JavaUtilCollectionContains() {
this.getDeclaringType() instanceof JavaUtilCollection and
this.hasStringSignature("contains(Object)")
}
}
请注意,我们使用 hasStringSignature 来检查
- 该方法的名称是
contains。 - 它只有一个参数。
- 参数的类型是
Object。
或者,我们可以使用 hasName、getNumberOfParameters 和 getParameter(0).getType() instanceof TypeObject 更详细地实现这三个检查。
与之前一样,最好通过运行一个简单的查询来测试新类,以选择所有 JavaUtilCollectionContains 实例;同样,应该只有一个结果。
现在,我们要识别所有对 Collection.contains 的调用,包括覆盖它的任何方法,并考虑 Collection 及其子类的所有参数化实例。也就是说,我们正在寻找方法访问,其中调用的方法的源声明(反射或传递)覆盖了 Collection.contains。我们在 CodeQL 类 JavaUtilCollectionContainsCall 中对其进行编码
class JavaUtilCollectionContainsCall extends MethodAccess {
JavaUtilCollectionContainsCall() {
exists(JavaUtilCollectionContains jucc |
this.getMethod().getSourceDeclaration().overrides*(jucc)
)
}
}
这个定义有点微妙,所以您应该运行一个简短的查询来测试 JavaUtilCollectionContainsCall 是否正确识别了对 Collection.contains 的调用。
对于对 contains 的每次调用,我们感兴趣的是两件事:参数的类型以及对其进行调用的集合的元素类型。因此,我们需要向类 JavaUtilCollectionContainsCall 添加两个成员谓词 getArgumentType 和 getCollectionElementType 来计算这些信息。
前者很容易
Type getArgumentType() {
result = this.getArgument(0).getType()
}
对于后者,我们按如下方式进行
- 找到正在调用的
contains方法的声明类型D。 - 找到
D的(反射或传递)超类型S,它是java.util.Collection的参数化实例。 - 返回
S的(唯一)类型参数。
我们按如下方式对其进行编码
Type getCollectionElementType() {
exists(RefType D, ParameterizedInterface S |
D = this.getMethod().getDeclaringType() and
D.hasSupertype*(S) and S.getSourceDeclaration() instanceof JavaUtilCollection and
result = S.getTypeArgument(0)
)
}
在向 JavaUtilCollectionContainsCall 添加了这两个成员谓词之后,我们需要编写一个谓词来检查两个给定的引用类型是否具有公共子类型
predicate haveCommonDescendant(RefType tp1, RefType tp2) {
exists(RefType commondesc | commondesc.hasSupertype*(tp1) and commondesc.hasSupertype*(tp2))
}
现在,我们已准备好编写第一个版本的查询
import java
// Insert the class definitions from above
from JavaUtilCollectionContainsCall juccc, Type collEltType, Type argType
where collEltType = juccc.getCollectionElementType() and argType = juccc.getArgumentType() and
not haveCommonDescendant(collEltType, argType)
select juccc, "Element type " + collEltType + " is incompatible with argument type " + argType
改进¶
对于许多程序,由于类型变量和通配符,此查询会产生大量的误报结果:例如,如果集合元素类型是一些类型变量 E,而参数类型是 String,那么 CodeQL 将认为两者没有公共子类型,我们的查询将标记该调用。排除此类误报结果的一种简单方法是简单地要求 collEltType 和 argType 均不是 TypeVariable 的实例。
另一个误报来源是基本类型的自动装箱:例如,如果集合的元素类型是 Integer,而参数的类型是 int,则谓词 haveCommonDescendant 将失败,因为 int 不是 RefType。为了解决这个问题,我们的查询应检查 collEltType 是否不是 argType 的装箱类型。
最后,null 很特殊,因为它的类型(在 CodeQL 库中称为 <nulltype>)与所有引用类型兼容,因此我们应将其排除在考虑范围之外。
添加了这三个改进之后,我们的最终查询变为
import java
class JavaUtilCollection extends GenericInterface {
JavaUtilCollection() {
this.hasQualifiedName("java.util", "Collection")
}
}
class JavaUtilCollectionContains extends Method {
JavaUtilCollectionContains() {
this.getDeclaringType() instanceof JavaUtilCollection and
this.hasStringSignature("contains(Object)")
}
}
class JavaUtilCollectionContainsCall extends MethodAccess {
JavaUtilCollectionContainsCall() {
exists(JavaUtilCollectionContains jucc |
this.getMethod().getSourceDeclaration().overrides*(jucc)
)
}
Type getArgumentType() {
result = this.getArgument(0).getType()
}
Type getCollectionElementType() {
exists(RefType D, ParameterizedInterface S |
D = this.getMethod().getDeclaringType() and
D.hasSupertype*(S) and S.getSourceDeclaration() instanceof JavaUtilCollection and
result = S.getTypeArgument(0)
)
}
}
predicate haveCommonDescendant(RefType tp1, RefType tp2) {
exists(RefType commondesc | commondesc.hasSupertype*(tp1) and commondesc.hasSupertype*(tp2))
}
from JavaUtilCollectionContainsCall juccc, Type collEltType, Type argType
where collEltType = juccc.getCollectionElementType() and argType = juccc.getArgumentType() and
not haveCommonDescendant(collEltType, argType) and
not collEltType instanceof TypeVariable and not argType instanceof TypeVariable and
not collEltType = argType.(PrimitiveType).getBoxedType() and
not argType.hasName("<nulltype>")
select juccc, "Element type " + collEltType + " is incompatible with argument type " + argType