调用图导航¶
CodeQL 包含用于识别调用其他代码的代码以及可以从其他地方调用的代码的类。这使您可以找到例如从未使用的代码。
调用图类¶
CodeQL 的 Java/Kotlin 库提供了两个用于表示程序调用图的抽象类:Callable
和 Call
。前者只是 Method
和 Constructor
的公共超类,后者是 MethodAccess
、ClassInstanceExpression
、ThisConstructorInvocationStmt
和 SuperConstructorInvocationStmt
的公共超类。简而言之,Callable
是可以调用的东西,Call
是调用 Callable
的东西。
例如,在以下程序中,所有可调用和调用都已用注释进行标注
class Super {
int x;
// callable
public Super() {
this(23); // call
}
// callable
public Super(int x) {
this.x = x;
}
// callable
public int getX() {
return x;
}
}
class Sub extends Super {
// callable
public Sub(int x) {
super(x+19); // call
}
// callable
public int getX() {
return x-19;
}
}
class Client {
// callable
public static void main(String[] args) {
Super s = new Sub(42); // call
s.getX(); // call
}
}
类 Call
提供了两个调用图导航谓词
getCallee
返回此调用(静态地)解析到的Callable
;请注意,对于对实例(即非静态)方法的调用,运行时调用的实际方法可能是覆盖此方法的其他方法。getCaller
返回此调用语法上属于其的一部分的Callable
。
例如,在我们的示例中,Client.main
中第二个调用的 getCallee
将返回 Super.getX
。但是,在运行时,此调用实际上会调用 Sub.getX
。
类 Callable
定义了大量的成员谓词;对于我们的目的,两个最重要的谓词是
calls(Callable target)
如果此可调用包含一个调用,其被调用者是target
,则成功。polyCalls(Callable target)
如果此可调用可能在运行时调用target
,则成功;如果它包含一个调用,其被调用者是target
或target
覆盖的方法,则为真。
在我们的示例中,Client.main
调用构造函数 Sub(int)
和方法 Super.getX
;此外,它 polyCalls
方法 Sub.getX
。
示例:查找未使用的代码¶
我们可以使用 Callable
类来编写一个查询,该查询查找未被任何其他方法调用的代码。
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee))
select callee
这个简单的查询通常会返回大量结果。
注意
我们必须在此使用
polyCalls
而不是calls
:我们希望合理地确保callee
未被调用,无论是直接调用还是通过覆盖。
在典型的 Java/Kotlin 项目上运行此查询会导致 Java/Kotlin 标准库中出现大量匹配项。这是有道理的,因为没有单个客户端程序使用标准库中的所有方法。更一般地说,我们可能希望排除来自已编译库的方法和构造函数。我们可以使用谓词 fromSource
来检查编译单元是否是源文件,并改进我们的查询
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource()
select callee, "Not called."
此更改减少了大多数代码库返回的结果数量。
我们可能还会注意到几个名称略微奇怪的未使用方法 <clinit>
:这些是类初始化程序;虽然它们在代码中没有显式调用,但在周围类加载时会隐式调用它们。因此,将它们从我们的查询中排除是有意义的。趁此机会,我们也可以排除析构函数,它们也以类似的方式被隐式调用
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize")
select callee, "Not called."
这也减少了大多数代码库返回的结果数量。
我们可能还想从查询中排除公共方法,因为它们可能是外部 API 入口点
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic()
select callee, "Not called."
这应该对返回的结果数量产生更明显的影响。
另一个特殊情况是非公共默认构造函数:例如,在单例模式中,为类提供私有的空默认构造函数以防止它被实例化。由于此类构造函数的唯一目的是不被调用,因此不应将其标记出来
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0
select callee, "Not called."
此更改对某些项目的成果有很大影响,但对其他项目的成果影响很小。不同项目之间使用这种模式的差异很大。
最后,在许多 Java 项目中,有通过反射间接调用的方法。因此,虽然没有调用这些方法,但它们实际上在使用。通常很难识别这些方法。但是,一个非常常见的特殊情况是 JUnit 测试方法,这些方法由测试运行器反射调用。CodeQL 的 Java 库支持识别 JUnit 和其他测试框架的测试类,我们可以利用它们来过滤掉这些类中定义的方法
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0 and
not callee.getDeclaringType() instanceof TestClass
select callee, "Not called."
这应该会进一步减少返回的结果数量。