CodeQL具有用于标识调用其他代码,以及可以被任意位置调用的代码的类。通过这个类你可以找到从未使用过的方法。
调用图类
CodeQL的Java库提供了两个抽象类来表示程序的调用图:Callable
和Call
。前者是Method
和Constructor的公共超类
,后者是MethodAccess
,ClassInstanceExpression
,ThisConstructorInvocationStmt
和SuperConstructorInvocationStmt的通用超类
。简而言之,Callable
是被调用的函数,而Call则是调用Callable
。
在下面的示例中,所有Callable
和Call都带有注释注解:
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
。
例如,在我们
第二个call的Client.main
的getCallee
示例中,将返回Super.getX
。不过,在运行时,此call实际上会调用Sub.getX
。
Callable类
定义了大量的成员谓词;就我们的目的而言,两个最重要的是:
calls(Callable target),
如果这个callable包含callee作为target的调用
。polyCalls(Callable target),
如果这个callable在运行时调用target
;如果它包含调用,该调用的callee为target
或target
覆写的方法,则为这种情况。
在我们的示例中,Client.main
calls构造函数Sub(int)
和Super.getX方法
;另外,它polyCalls
Sub.getX方法
。
示例:查找未使用的方法
我们可以使用Callable
类来编写查询,以查找未被任何其他方法调用的方法:
import java from Callable callee where not exists(Callable caller | caller.polyCalls(callee)) select callee
➤在LGTM.com上的查询控制台中查看此内容。这个简单的查询通常返回大量结果。
笔记
这里我们必须使用
polyCalls
而不是calls
:我们想要合理地确保callee
不会被直接或通过覆写方法调用。
在典型的Java项目上运行此查询会导致Java标准库中出现很多匹配项。这是有道理的,因为没有单个客户端程序会去使用标准库的每一个方法。更一般而言,我们可能要从编译的库中排除方法和构造函数。我们可以使用谓词fromSource
来检查编译单元是否为源文件,并优化查询:
import java from Callable callee where not exists(Callable caller | caller.polyCalls(callee)) and callee.getCompilationUnit().fromSource() select callee, "Not called."
➤在LGTM.com上的查询控制台中查看此内容。此更改减少了大多数项目返回的结果数。
我们可能还会注意到一些名称有些陌生的未使用的方法<clinit>
:这些是类初始化器;尽管在代码中的任何地方都没有显式调用它们,但是在加载周围的类时会隐式调用它们。因此,将它们从我们的查询中排除是有意义的。
在此过程中,我们还可以排除finalize,finalize也会被隐式调用:
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."
➤在LGTM.com上的查询控制台中查看此内容。这也减少了大多数项目返回的结果数量。
我们可能还希望从查询中排除public方法,因为它们可能是外部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."
➤在LGTM.com上的查询控制台中查看此内容。这应该对返回的结果数量产生更明显的影响。
另一个特殊情况是非public默认构造函数:例如,在单例模式中,为类提供了私有的空默认构造函数,以防止被实例化。由于此类构造函数的真正目的是不被调用,因此不应对其进行标记:
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."
➤在LGTM.com上的查询控制台中查看此内容。此更改对某些项目的结果影响很大,而对其他项目的结果影响很小。在不同项目之间,此模式的使用差异很大。
最后,在许多Java项目中,有一些方法是通过反射间接调用的。因此,尽管没有calls调用这些方法,但实际上它们是被使用的。通常很难识别这种方法。但是,一个非常常见的特殊情况是JUnit测试方法,由测试运行器反射地调用。Java的CodeQL库支持识别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."
➤在LGTM.com上的查询控制台中查看此内容。这样可以进一步减少返回结果的数量。